Skip to content

C++ Move Semantics

Move semantics enable efficient transfer of resources without unnecessary copies. This guide builds understanding from first principles.


OperationWhat HappensCost
v.push_back(s)Full copy of s into vectorExpensive — allocate + copy bytes
v.push_back(std::move(s))Transfer ownership to vector, s now emptyCheap — swap a pointer

std::vector::push_back has two overloads:

void push_back(const T& value); // Called for: lvalues
void push_back(T&& value); // Called for: xvalues, prvalues
Caller IntentCodeOverloadCost
Keep originalv.push_back(item)const T&1 copy
Give up originalv.push_back(std::move(item))T&&1 move
Temporaryv.push_back(Foo{})T&&1 move

ScenarioCostProblem
Keep original1 copy ✅
Give up original1 copy ❌Wasteful! Could have moved
Temporary1 copy ❌Wasteful!
ScenarioCostProblem
Keep original2 opsMust copy to temp first
Give up original1 move ✅
Temporary1 move ✅

Why 2 ops? Lvalue won’t compile directly:

v.push_back(item); // ❌ Won't compile
v.push_back(Foo(item)); // ✅ Workaround: copy→temp, move→vector

The deeper problem: API ergonomics. With only T&&, the caller pays for the design:

string original = "keep me";
vec.push_back(original); // ❌ Won't compile!
vec.push_back(string(original)); // ✅ Ugly—explicit copy
vec.push_back(std::move(original)); // ✅ But original is gone!

With dual methods, the caller chooses without contortions:

  • push_back(x) → “copy, I’m keeping it”
  • push_back(std::move(x)) → “take it, I’m done”

Without const T&, you force callers to make explicit copies when they want to keep their value—ugly and error-prone.

ScenarioCost
Keep original1 copy ✅
Give up original1 move ✅
Temporary1 move ✅

Every path is optimal.


template<typename T>
class SimpleVector {
T* data_;
size_t size_;
public:
// COPY: construct T by copying from value
void push_back(const T& value) {
new (&data_[size_++]) T(value); // Copy constructor
}
// MOVE: construct T by moving from value
void push_back(T&& value) {
new (&data_[size_++]) T(std::move(value)); // Move constructor
}
};

Every C++ expression has a value category:

expression
┌──────────┴──────────┐
│ │
glvalue rvalue
"has identity" "can move from"
│ │
┌────┴────┐ ┌────┴────┐
│ │ │ │
lvalue xvalue xvalue prvalue
▲ ▲
└───────────────┘
(same thing)
CategoryHas Identity?Moveable?Examples
lvalue❌*x, arr[i], *ptr
xvaluestd::move(x), static_cast<T&&>(x)
prvalue42, Foo{}, x + y

*Unless explicitly cast with std::move()

TermStands ForWhy It’s Confusing
lvalueLeft of =Can appear on right too!
rvalueRight of =Includes two different things (xvalue + prvalue)
glvalueGeneralized lvalueNobody uses this term
prvaluePure rvalueBetter than “rvalue”
xvalueExpiring valueActually clear!

Standard Terminology (Used in This Document)

Section titled “Standard Terminology (Used in This Document)”

This document uses the standard C++11 terminology:

CategoryDefinitionOverload SelectedExamples
lvalueHas identity, cannot movefoo(const T&)x, arr[i], *ptr
xvalueHas identity, can movefoo(T&&)std::move(x)
prvalueNo identity, can movefoo(T&&)42, Foo{}, x + y
rvaluexvalue OR prvaluefoo(T&&)(composite category)
Official/Confusing TermBetter NameMeaning
”rvalue reference” (T&&)Move referenceParameter that can bind to rvalues
T&& parameter in non-templateSink parameter”I will consume this value”
T&& in template (deduced T)Forwarding referenceCan bind to anything
std::move(x)xvalue castMarks lvalue as moveable

Identity = You can take its address with &

int x = 42;
&x; // ✅ x has identity — lives at an address
&42; // ❌ Error: literal has no address
&(x + 1); // ❌ Error: temporary has no address

template<typename T>
T&& move(T& value) {
return static_cast<T&&>(value);
}

Why T& value (lvalue reference)?

std::move is designed for named objects (lvalues) — things you want to explicitly give up ownership of. Lvalues only bind to T&, not T&&.

You wouldn’t call std::move on a temporary — it’s already an rvalue! So the parameter being T& naturally filters to its intended use case: converting lvalues to xvalues.

ExpressionValue Category
xlvalue
std::move(x)xvalue
static_cast<T&&>(x)xvalue

std::move generates zero code. It just changes the type for overload resolution.

class string {
char* data_;
string(string&& other) { // Move constructor
data_ = other.data_; // Steal pointer
other.data_ = nullptr; // Leave empty
}
};
ComponentRuntime Cost
std::move(x)Zero — just a type cast
Move constructorCheap — pointer swap
Copy constructorExpensive — memory allocation

Critical: Parameter Type Inside a Function

Section titled “Critical: Parameter Type Inside a Function”

Inside a function, a T&& parameter is an lvalue!

void foo(std::string&& s) {
// Here, `s` is an LVALUE (it has a name!)
bar(s); // Calls bar(const T&) — s is lvalue
bar(std::move(s)); // Calls bar(T&&) — explicitly cast to xvalue
}
ExpressionWhereCategoryWhy
std::move(x)At call sitexvalueCast produces xvalue
s inside foo(T&& s)Inside functionlvalueNamed variable
std::move(s) inside functionInside functionxvalueExplicit cast

You might think: “Isn’t it wasteful to call std::move() repeatedly as values pass through functions?”

No, because std::move() has zero runtime cost. It’s a compile-time cast only.

The real question is: “Why not make T&& parameters automatically moveable?”

Answer: Safety. If s were automatically an xvalue, you could accidentally move twice:

void foo(string&& s) {
bar(s); // If this moved automatically...
baz(s); // ...this would use a moved-from object! Silent bug!
}

By making it an lvalue, each move is explicit and visible:

void foo(string&& s) {
bar(std::move(s)); // Clearly moves here
baz(s); // Compiler still allows this, but code review catches it
}

The cost of typing std::move() is nothing compared to debugging use-after-move bugs.


Yes! An xvalue can be assigned to:

std::string s = "hello";
std::move(s) = "world"; // ✅ Legal!

s is fully usable and contains "world". The std::move() produced an xvalue reference to s, but the assignment operator just assigned normally through that reference.

std::string s = "hello";
std::move(s) = "world";
std::cout << s; // Prints "world" — s is fine!

This is rarely useful in practice. It’s more of a C++ curiosity than a pattern you’d use.

xvalue on the right enables move construction/assignment:

std::string target = std::move(source); // Move constructor
target = std::move(source); // Move assignment
// After either: source is in "valid but unspecified" state

After being moved from, source is in a valid but unspecified state:

  • You CAN assign to it, destroy it, or call methods with no preconditions
  • You SHOULD NOT read its value (undefined what it contains)

You want to write a wrapper that forwards arguments to another function:

void process(const string& s) { cout << "copy: " << s; }
void process(string&& s) { cout << "move: " << s; }
template<typename T>
void wrapper(T&& arg) {
process(arg); // What gets called?
}
int main() {
string s = "hello";
wrapper(s); // Caller sends lvalue
wrapper(std::move(s)); // Caller sends xvalue
wrapper(string{"temp"}); // Caller sends prvalue
}

Actual output:

copy: hello
copy: hello ← WRONG! Caller sent xvalue, expected move
copy: temp ← WRONG! Caller sent prvalue, expected move

Why? Inside wrapper, arg is an lvalue (it has a name!). So process(arg) always calls the copy version.

template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg)); // Preserves original category
}

Now output is correct:

copy: hello ← lvalue → copy
move: hello ← xvalue → move
move: temp ← prvalue → move

In the example above, T&& arg is NOT a regular move reference. It’s a forwarding reference (also called “universal reference” by Scott Meyers).

ContextT&& is calledBehavior
Non-template: void foo(string&& s)Move referenceOnly binds to rvalues
Template: void foo(T&& arg)Forwarding referenceBinds to ANYTHING

With a forwarding reference, the compiler deduces T differently based on what you pass:

You passT deduced asT&& becomesCategory of arg
lvalue xstring&string& &&string&lvalue
xvalue std::move(x)stringstring&&lvalue (has name!)
prvalue string{}stringstring&&lvalue (has name!)

This is reference collapsing: & &&&, && &&&&

🤔 Intuition Behind the Rules: Why does & “win”?

Reference collapsing is an explicit rule in the C++ standard — the compiler implements it specifically. But the design choice has solid intuition:

Think of & as “sticky” — lvalue-ness is contagious:

CombinationResultWhy?
& &&lvalue + anything = lvalue
& &&&lvalue + anything = lvalue
&& &&anything + lvalue = lvalue
&& &&&&Only rvalue + rvalue = rvalue

The safety principle: If at any point in the reference chain something is an lvalue (meaning someone else might have a reference to it), you can’t safely treat it as moveable. The & “contaminates” the chain.

Think of it like logical AND for “can I move this?”:

move_ok && move_ok = move_ok (&&)
keep && move_ok = keep (&)
move_ok && keep = keep (&)
keep && keep = keep (&)

Only when both references are && (both saying “this is temporary/moveable”) can the result remain &&.

The template parameter T captures the original category:

Caller writesT deduced asstd::forward<T>(arg) produces
wrapper(x) (lvalue)string&lvalue
wrapper(std::move(x)) (rvalue)stringxvalue
wrapper(string{}) (prvalue)stringxvalue
template<typename T>
T&& forward(std::remove_reference_t<T>& arg) noexcept {
return static_cast<T&&>(arg);
}

Why std::remove_reference_t<T>& arg?

When T is string& (from forwarding reference deduction), writing just T& arg would give string& & arg — a reference to a reference, which isn’t directly expressible.

std::remove_reference_t<T> strips any reference from T first:

  • T = string&remove_reference_t<T> = string → parameter: string& arg
  • T = stringremove_reference_t<T> = string → parameter: string& arg

This gives a consistent lvalue reference parameter regardless of whether T was deduced as an lvalue or rvalue reference. The return type T&& then uses reference collapsing to produce the correct result type.

🤓 Compiler Nerd Corner: How is remove_reference_t implemented?

It’s not a compiler intrinsic — it’s pure template metaprogramming using partial template specialization:

// Primary template: T has no reference
template<typename T>
struct remove_reference {
using type = T;
};
// Specialization: T is an lvalue reference
template<typename T>
struct remove_reference<T&> {
using type = T;
};
// Specialization: T is an rvalue reference
template<typename T>
struct remove_reference<T&&> {
using type = T;
};
// Helper alias (C++14+)
template<typename T>
using remove_reference_t = typename remove_reference<T>::type;

The compiler pattern-matches T& or T&& against the input type, extracting the underlying T:

Input TypeMatches Specialization::type Result
intPrimary templateint
int&T& specializationint
int&&T&& specializationint

This is the same technique used for std::decay, std::add_pointer, std::conditional, etc. — no compiler magic required, just elegant type pattern matching! ✨

The magic is in reference collapsing rules:

  • When T = string&: static_cast<string& &&>string& (lvalue)
  • When T = string: static_cast<string&&>string&& (xvalue)
Term in This ContextFormal DefinitionEffect on Overload
lvalueglvalue that’s NOT xvalueCalls foo(const T&)
rvaluexvalue OR prvalueCalls foo(T&&)
xvalueExpiring lvalueCalls foo(T&&)

So yes, rvalue includes xvalue. When std::forward “produces an rvalue,” it really produces an xvalue (because it’s a cast on a named object).

FunctionUse CaseAlways produces rvalue?
std::move(x)”I’m done with this, take it”✅ Yes (xvalue)
std::forward<T>(x)”Pass along whatever I received”❌ Depends on T
SituationUseWhy
Regular function, done with valuestd::move(x)Always produces xvalue
Template with T&&, forwardingstd::forward<T>(x)Preserves original category
Template with T&&, consumingstd::move(x)You want to move regardless

1. Forwarding references ONLY work with type deduction:

template<typename T>
void foo(T&& arg); // ✅ Forwarding reference (T is deduced)
void bar(string&& arg); // ❌ Just a move reference (no deduction)
template<typename T>
void baz(vector<T>&& v); // ❌ Move reference! (T isn't the && type)

2. auto&& is also a forwarding reference:

auto&& x = foo(); // Forwarding reference! Binds to anything.

3. Don’t forward twice:

template<typename T>
void wrapper(T&& arg) {
foo(std::forward<T>(arg));
bar(std::forward<T>(arg)); // ⚠️ Dangerous if foo moved from arg!
}

4. Named parameters are always lvalues:

template<typename T>
void wrapper(T&& arg) {
// arg is ALWAYS an lvalue here (it has a name)
// Even if caller passed an rvalue!
// Use std::forward<T>(arg) to restore original category
}

At machine level, T& and T&& are identical — both are memory addresses:

void foo(string& s); // Compiles to: foo(string* s)
void bar(string&& s); // Compiles to: bar(string* s) ← Same!

The difference exists only at compile time for overload resolution:

  1. Compiler sees v.push_back(std::move(x))
  2. Expression type is string&& (xvalue)
  3. Overload resolution picks push_back(T&&)
  4. That function’s body performs the move

The calling code is identical. Only which function gets called differs.


Not really. It’s misleading:

string s = "hello";
string&& ref = std::move(s); // "rvalue reference" to an lvalue!
Official TermBetter Mental Model
rvalue referenceMove reference
T&& parameterSink parameter

ConceptKey Point
Dual-methodTwo overloads → optimal for all callers
std::moveJust a cast, zero runtime cost
Move constructorDoes the actual work (pointer swap)
ReferencesBoth & and && are pointers at machine level
T&& parameterInside function, it’s an lvalue (has a name)
xvaluelvalue marked as “steal from me”
std::forwardPreserves original value category in templates