C++ Move Semantics
This content is not available in your language yet.
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 |