Debug Foundry

Why copy the whole block when the old owner is done with it?

Drag to turn · pinch or ctrl-scroll to zoom · pinch to zoom

Finished state of the move semantics simulation: two owner boxes and one heap block, the ownership arrow shifted from the first owner to the second, the first owner shown empty

Two owners
one block each

Replay the ending with
auto a = std::make_unique<Block>();  // a owns one block
Block d = *a;                        // copy: a second block exists
auto b = std::move(a);                // move construct: same block, a empty
auto c = std::make_unique<Block>();  // c owns its own block
c = std::move(b);                     // move assign: c's old block dies first
Read the chapter

What it's for

  • Every hand-off of a heavy resource: a vector returned from a function, a string rebuilt in a loop, a unique_ptr passed into its new owner. Move semantics is how C++ avoids copying what the source is about to abandon.
  • The sequel to smart pointers and RAII: those chapters made ownership automatic; this one makes ownership transferable — same block, new owner, no duplicate.

Good to know

The first surprise: std::move doesn't move anything. It's a cast — it marks an object as about-to-die so the compiler is allowed to pick a move overload. The move itself is a constructor or assignment operator, and if no move overload fits — say the source is const, or the type has none — the call quietly falls back to copying. The example type here is std::unique_ptr: moving one transfers the stored pointer and leaves the source empty. That emptiness is unique_ptr's own guarantee, not a law — a moved-from standard object is only promised to be valid-but-unspecified, and your own types make their own contracts. Construction and assignment differ: move construction builds a fresh owner around the resource, while move assignment runs on an owner that may already hold one — its old resource is released first, then the transfer happens, so nothing leaks and nothing is released twice. The source's destructor still runs afterward, on the empty object; a moved-from object must stay destructible. Two traps the simulation can't show: a named rvalue reference is itself an lvalue — inside void f(Block&& b), plain b won't move, you must cast again; and a raw owning pointer with a defaulted move copies the pointer without nulling the source — two owners, one delete each, a double-free (the rule of zero exists for this). Copy elision is a separate mechanism that can skip the move entirely — which is why return std::move(local) can hurt: it can block NRVO and force a move that elision would have avoided. And in containers, relocation prefers moves only when they're noexcept (that's std::move_if_noexcept's whole job); a throwing move constructor makes vector fall back to copying to keep its strong exception guarantee.

Why the steps go in this order

  • The copy comes first: two live owners, two blocks — correct, safe, and exactly the cost the chapter is about to remove.
  • The cast is named before any transfer: std::move only enables the overload, and seeing it separated from the transfer is what kills the "std::move moves things" myth.
  • Move construction before move assignment: construction is the clean case (a new owner, one block, identity preserved); assignment adds the old destination's cleanup, shown as its own beat so no release is ever ambiguous.
  • The source's destructor runs last on screen: the moved-from object isn't gone, it's empty — and that distinction is the one reviewers actually check.

Before you rely on it

This is a toy: two owner boxes, one heap block, and arrows for ownership. The code card is real C++ and unique_ptr's behavior — transfer on move, empty source, old destination released first on assignment — is its documented contract, not something the simulation measured. What the toy flattens: moved-from guarantees are type-specific (empty is unique_ptr's promise, not everyone's); moves aren't automatically cheap or O(1); const sources and missing overloads fall back to copying; and containers move only when the type plays along with noexcept. Perfect forwarding and reference collapsing are deliberately left out — they're their own chapter.

References