Move Semantics & Rvalue References

Course 2 · Ch 5
Move Semantics & Rvalue References
std::move doesn't move anything — it's a cast. The real mechanism, revealed.

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.

class Buffer { public: int *data; Buffer(Buffer &&other) { // move constructor data = other.data; other.data = nullptr; // prevents other's destructor from double-freeing } };

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.

Buffer a; Buffer b = std::move(a); // casts a to an rvalue reference — selects the move constructor

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.

ConceptC++Rust
Moving instead of copyingopt-in — std::move, or compiler RVOautomatic and default — per rust1-3's ownership model
Forgetting to move efficientlygenuinely possible — an unnecessary copy happens silentlynot possible — the compiler always moves by default
Using a moved-from valuea logic bug (valid but unspecified), not UBa compile error — the compiler tracks it
Modern compilers often apply Return Value Optimization automatically
A function returning a local object by value frequently doesn't need an explicit 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.
Using an object's original value after moving from it is a real logic bug
Calling 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

Challenge 1

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 solution
Challenge 2

Move 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 solution
Challenge 3

Explain 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 solution

Chapter 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::move is 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::move or relying on RVO
  • Next chapter: exception handling — try/catch/throw, and RAII's role in exception safety