Move Semantics & Rvalue References
cpp2-4 used std::move to transfer a unique_ptr's ownership without explaining why it worked. This chapter is that explanation, in full.
The Problem — Unnecessary Copies
Returning a large object like a vector by value historically meant a full, expensive deep copy. Before C++11, this was a genuine performance problem, pushing programmers toward awkward workarounds — passing output parameters by reference instead of simply returning by value.
lvalues and rvalues
A necessary vocabulary: an lvalue has a name and persistent identity — an ordinary variable. An rvalue is a temporary with no persistent identity — a literal, or a function's return value. T& binds only to lvalues; T&& (a new syntax — a rvalue reference) binds only to rvalues. This distinction is exactly what lets the compiler choose between a copy and a move, at compile time, based on which kind of value is actually being used.
Move Constructors and Move Assignment
A class can define a move constructor, taking an rvalue reference. Instead of copying the source object's resources, it steals them — pointer/handle fields are copied over, then explicitly nulled out in the source, so the source's own destructor doesn't also try to free the same resource. This directly avoids c2-3's own double-free bug class, by deliberate design.
std::move — Casting an lvalue to an rvalue
The real mechanism: std::move doesn't actually move anything by itself — it's purely a cast, converting an lvalue (which would normally bind to T&, triggering a copy) into an rvalue reference (T&&), so the move constructor gets selected instead of the copy constructor. A genuinely important, often-misunderstood fact: std::move performs no action of its own at all.
After a Move, the Source Is in a "Valid But Unspecified" State
A precise, real rule: a moved-from object is guaranteed to still be safely destructible and (usually) safely reassignable — but its actual value is unspecified. Reading it for anything else is a logic bug, though not undefined behavior the way c1-7's dangling pointer would be — a genuinely important, precise distinction.
Contrasted With Rust's Own Move-By-Default Semantics
Rust performs this exact optimization automatically and by default, per rust1-3's own ownership model — passing or returning a value moves it unless the type is Copy, with no special syntax needed and no possibility of forgetting to move efficiently. C++ requires explicitly opting in via std::move, or relying on the compiler's own automatic Return Value Optimization in specific cases — another point where Rust simply enforces, by default, a discipline C++ makes available but optional.
| Concept | C++ | Rust |
|---|---|---|
| Moving instead of copying | opt-in — std::move, or compiler RVO | automatic and default — per rust1-3's ownership model |
| Forgetting to move efficiently | genuinely possible — an unnecessary copy happens silently | not possible — the compiler always moves by default |
| Using a moved-from value | a logic bug (valid but unspecified), not UB | a compile error — the compiler tracks it |
std::move at all — the compiler elides the copy/move entirely in many cases. Worth knowing so as not to sprinkle std::move defensively where it isn't needed.
std::move(a) and then continuing to read a's value afterward (rather than only destroying or reassigning it) silently reads whatever unspecified state the object was left in. Not undefined behavior — but definitely wrong.
Coding Challenges
Write a class with a raw pointer member and a move constructor that steals the pointer and nulls out the source. Move-construct one instance from another, then print a message confirming the source's pointer is now nullptr.
📄 View solutionMove a std::string into another variable with std::move, then print the moved-from string's value. Explain what you observe and why reading it afterward is a logic bug rather than undefined behavior.
📄 View solutionExplain precisely why std::move is described as "performing no action of its own" — what does it actually do at the type-system level, and what causes the move constructor to actually run afterward?
📄 View solutionChapter 5 Quick Reference
- lvalue — has a name/identity; rvalue — a temporary with none
T&&— an rvalue reference, binds only to rvalues, enables move constructors/assignment- A move constructor steals resources and nulls the source, avoiding a double-free
std::moveis purely a cast to an rvalue reference — it moves nothing by itself- A moved-from object is valid but unspecified — safe to destroy/reassign, a logic bug (not UB) to read otherwise
- Rust moves automatically by default; C++ requires opting in via
std::moveor relying on RVO - Next chapter: exception handling — try/catch/throw, and RAII's role in exception safety