C++ Move Semantics
Move semantics enable efficient transfer of resources without unnecessary copies. This guide builds understanding from first principles.
Why Move Semantics Exist
Section titled “Why Move Semantics Exist”| Operation | What Happens | Cost |
|---|---|---|
v.push_back(s) | Full copy of s into vector | Expensive — allocate + copy bytes |
v.push_back(std::move(s)) | Transfer ownership to vector, s now empty | Cheap — swap a pointer |
The Dual-Method Pattern
Section titled “The Dual-Method Pattern”std::vector::push_back has two overloads:
void push_back(const T& value); // Called for: lvaluesvoid push_back(T&& value); // Called for: xvalues, prvaluesAll Caller Scenarios
Section titled “All Caller Scenarios”| Caller Intent | Code | Overload | Cost |
|---|---|---|---|
| Keep original | v.push_back(item) | const T& | 1 copy |
| Give up original | v.push_back(std::move(item)) | T&& | 1 move |
| Temporary | v.push_back(Foo{}) | T&& | 1 move |
What If Only One Overload Existed?
Section titled “What If Only One Overload Existed?”Design A: Only const T&
Section titled “Design A: Only const T&”| Scenario | Cost | Problem |
|---|---|---|
| Keep original | 1 copy ✅ | — |
| Give up original | 1 copy ❌ | Wasteful! Could have moved |
| Temporary | 1 copy ❌ | Wasteful! |
Design B: Only T&&
Section titled “Design B: Only T&&”| Scenario | Cost | Problem |
|---|---|---|
| Keep original | 2 ops ❌ | Must copy to temp first |
| Give up original | 1 move ✅ | — |
| Temporary | 1 move ✅ | — |
Why 2 ops? Lvalue won’t compile directly:
v.push_back(item); // ❌ Won't compilev.push_back(Foo(item)); // ✅ Workaround: copy→temp, move→vectorThe 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 copyvec.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.
Design C: Both (Optimal)
Section titled “Design C: Both (Optimal)”| Scenario | Cost |
|---|---|
| Keep original | 1 copy ✅ |
| Give up original | 1 move ✅ |
| Temporary | 1 move ✅ |
Every path is optimal.
Inside a Simple Vector
Section titled “Inside a Simple Vector”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 }};Value Categories
Section titled “Value Categories”Every C++ expression has a value category:
expression │ ┌──────────┴──────────┐ │ │ glvalue rvalue "has identity" "can move from" │ │ ┌────┴────┐ ┌────┴────┐ │ │ │ │ lvalue xvalue xvalue prvalue ▲ ▲ └───────────────┘ (same thing)Quick Reference
Section titled “Quick Reference”| Category | Has Identity? | Moveable? | Examples |
|---|---|---|---|
| lvalue | ✅ | ❌* | x, arr[i], *ptr |
| xvalue | ✅ | ✅ | std::move(x), static_cast<T&&>(x) |
| prvalue | ❌ | ✅ | 42, Foo{}, x + y |
*Unless explicitly cast with std::move()
Name Origins (Legacy Terms)
Section titled “Name Origins (Legacy Terms)”| Term | Stands For | Why It’s Confusing |
|---|---|---|
| lvalue | Left of = | Can appear on right too! |
| rvalue | Right of = | Includes two different things (xvalue + prvalue) |
| glvalue | Generalized lvalue | Nobody uses this term |
| prvalue | Pure rvalue | Better than “rvalue” |
| xvalue | Expiring value | Actually clear! |
Standard Terminology (Used in This Document)
Section titled “Standard Terminology (Used in This Document)”This document uses the standard C++11 terminology:
| Category | Definition | Overload Selected | Examples |
|---|---|---|---|
| lvalue | Has identity, cannot move | foo(const T&) | x, arr[i], *ptr |
| xvalue | Has identity, can move | foo(T&&) | std::move(x) |
| prvalue | No identity, can move | foo(T&&) | 42, Foo{}, x + y |
| rvalue | xvalue OR prvalue | foo(T&&) | (composite category) |
Better Names for Common Patterns
Section titled “Better Names for Common Patterns”| Official/Confusing Term | Better Name | Meaning |
|---|---|---|
”rvalue reference” (T&&) | Move reference | Parameter that can bind to rvalues |
T&& parameter in non-template | Sink parameter | ”I will consume this value” |
T&& in template (deduced T) | Forwarding reference | Can bind to anything |
std::move(x) | xvalue cast | Marks lvalue as moveable |
What “Has Identity” Means
Section titled “What “Has Identity” Means”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 addressHow std::move Works
Section titled “How std::move Works”It’s Just a Cast
Section titled “It’s Just a Cast”template<typename T>T&& move(T& value) { return static_cast<T&&>(value);}Why
T& value(lvalue reference)?
std::moveis designed for named objects (lvalues) — things you want to explicitly give up ownership of. Lvalues only bind toT&, notT&&.You wouldn’t call
std::moveon a temporary — it’s already an rvalue! So the parameter beingT&naturally filters to its intended use case: converting lvalues to xvalues.
| Expression | Value Category |
|---|---|
x | lvalue |
std::move(x) | xvalue |
static_cast<T&&>(x) | xvalue |
std::move generates zero code. It just changes the type for overload resolution.
The Move Constructor Does Real Work
Section titled “The Move Constructor Does Real Work”class string { char* data_;
string(string&& other) { // Move constructor data_ = other.data_; // Steal pointer other.data_ = nullptr; // Leave empty }};| Component | Runtime Cost |
|---|---|
std::move(x) | Zero — just a type cast |
| Move constructor | Cheap — pointer swap |
| Copy constructor | Expensive — 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}| Expression | Where | Category | Why |
|---|---|---|---|
std::move(x) | At call site | xvalue | Cast produces xvalue |
s inside foo(T&& s) | Inside function | lvalue | Named variable |
std::move(s) inside function | Inside function | xvalue | Explicit cast |
Why This Design? (Safety vs Efficiency)
Section titled “Why This Design? (Safety vs Efficiency)”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.
Can xvalue Appear on Left of =?
Section titled “Can xvalue Appear on Left of =?”Yes! An xvalue can be assigned to:
std::string s = "hello";std::move(s) = "world"; // ✅ Legal!What Happens After?
Section titled “What Happens After?”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.
The Common Pattern
Section titled “The Common Pattern”xvalue on the right enables move construction/assignment:
std::string target = std::move(source); // Move constructortarget = std::move(source); // Move assignment// After either: source is in "valid but unspecified" stateAfter 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)
std::forward — Perfect Forwarding
Section titled “std::forward — Perfect Forwarding”Motivating Example: The Problem
Section titled “Motivating Example: The Problem”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: hellocopy: hello ← WRONG! Caller sent xvalue, expected movecopy: temp ← WRONG! Caller sent prvalue, expected moveWhy? Inside wrapper, arg is an lvalue (it has a name!). So process(arg) always calls the copy version.
The Solution: std::forward
Section titled “The Solution: std::forward”template<typename T>void wrapper(T&& arg) { process(std::forward<T>(arg)); // Preserves original category}Now output is correct:
copy: hello ← lvalue → copymove: hello ← xvalue → movemove: temp ← prvalue → moveWait, What Is T&& in a Template?
Section titled “Wait, What Is T&& in a Template?”In the example above, T&& arg is NOT a regular move reference. It’s a forwarding reference (also called “universal reference” by Scott Meyers).
| Context | T&& is called | Behavior |
|---|---|---|
Non-template: void foo(string&& s) | Move reference | Only binds to rvalues |
Template: void foo(T&& arg) | Forwarding reference | Binds to ANYTHING |
Why the Different Behavior?
Section titled “Why the Different Behavior?”With a forwarding reference, the compiler deduces T differently based on what you pass:
| You pass | T deduced as | T&& becomes | Category of arg |
|---|---|---|---|
lvalue x | string& | string& && → string& | lvalue |
xvalue std::move(x) | string | string&& | lvalue (has name!) |
prvalue string{} | string | string&& | 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:
| Combination | Result | Why? |
|---|---|---|
& & | & | 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 &&.
How Does It Know?
Section titled “How Does It Know?”The template parameter T captures the original category:
| Caller writes | T deduced as | std::forward<T>(arg) produces |
|---|---|---|
wrapper(x) (lvalue) | string& | lvalue |
wrapper(std::move(x)) (rvalue) | string | xvalue |
wrapper(string{}) (prvalue) | string | xvalue |
How It’s Defined
Section titled “How It’s Defined”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
Tisstring&(from forwarding reference deduction), writing justT& argwould givestring& & arg— a reference to a reference, which isn’t directly expressible.
std::remove_reference_t<T>strips any reference fromTfirst:
T = string&→remove_reference_t<T>=string→ parameter:string& argT = string→remove_reference_t<T>=string→ parameter:string& argThis gives a consistent lvalue reference parameter regardless of whether
Twas deduced as an lvalue or rvalue reference. The return typeT&&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 referencetemplate<typename T>struct remove_reference { using type = T;};
// Specialization: T is an lvalue referencetemplate<typename T>struct remove_reference<T&> { using type = T;};
// Specialization: T is an rvalue referencetemplate<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 Type | Matches Specialization | ::type Result |
|---|---|---|
int | Primary template | int |
int& | T& specialization | int |
int&& | T&& specialization | int |
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)
Terminology Clarification
Section titled “Terminology Clarification”| Term in This Context | Formal Definition | Effect on Overload |
|---|---|---|
| lvalue | glvalue that’s NOT xvalue | Calls foo(const T&) |
| rvalue | xvalue OR prvalue | Calls foo(T&&) |
| xvalue | Expiring lvalue | Calls 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).
std::move vs std::forward
Section titled “std::move vs std::forward”| Function | Use Case | Always 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 |
When to Use Which?
Section titled “When to Use Which?”| Situation | Use | Why |
|---|---|---|
| Regular function, done with value | std::move(x) | Always produces xvalue |
Template with T&&, forwarding | std::forward<T>(x) | Preserves original category |
Template with T&&, consuming | std::move(x) | You want to move regardless |
Caveats and Gotchas
Section titled “Caveats and Gotchas”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}The Compiler’s Perspective
Section titled “The Compiler’s Perspective”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:
- Compiler sees
v.push_back(std::move(x)) - Expression type is
string&&(xvalue) - Overload resolution picks
push_back(T&&) - That function’s body performs the move
The calling code is identical. Only which function gets called differs.
Is “rvalue reference” a Good Name?
Section titled “Is “rvalue reference” a Good Name?”Not really. It’s misleading:
string s = "hello";string&& ref = std::move(s); // "rvalue reference" to an lvalue!| Official Term | Better Mental Model |
|---|---|
| rvalue reference | Move reference |
| T&& parameter | Sink parameter |
Key Takeaways
Section titled “Key Takeaways”| Concept | Key Point |
|---|---|
| Dual-method | Two overloads → optimal for all callers |
std::move | Just a cast, zero runtime cost |
| Move constructor | Does the actual work (pointer swap) |
| References | Both & and && are pointers at machine level |
T&& parameter | Inside function, it’s an lvalue (has a name) |
| xvalue | lvalue marked as “steal from me” |
std::forward | Preserves original value category in templates |