5.2.9 · D2C++ Programming

Visual walkthrough — Move semantics — rvalue references (&&), std - move, std - forward

1,857 words8 min readBack to topic

Step 1 — What is an object that "owns" a buffer?

WHAT. We separate the small "handle" (the object itself) from the big "buffer" (the heap memory it points to).

WHY. Copying is expensive only because of the big box. If we can hand over the arrow without re-copying the big box, we win. So we must picture them separately from line one.

PICTURE. The object is two tiny cells; the arrow data reaches across to a large heap block holding the characters.

Figure — Move semantics — rvalue references (&&), std - move, std - forward

Step 2 — What a deep copy actually costs

WHAT. We drew the full price of a copy: one allocation + one byte-by-byte march over the whole buffer.

WHY show the cost first? Because move semantics is only interesting as a way to skip exactly these two lines. You cannot appreciate the shortcut until you see the long road.

PICTURE. Source and destination each have their own big heap box; the amber arrows show every byte being duplicated.

Figure — Move semantics — rvalue references (&&), std - move, std - forward

Step 3 — The insight: sometimes the source is about to die

WHAT. We split every expression into two families: named survivors (lvalues) and nameless temporaries (rvalues).

WHY this split matters. If the source is a nameless temporary, nobody will ever read it again — it dies at the end of the statement. Copying its buffer and then immediately deleting the original is pure waste. That is the whole opening.

PICTURE. On the left, a named object a standing on solid ground. On the right, a temporary drawn as a fading ghost with a tombstone: it will be destroyed at the ;.

Figure — Move semantics — rvalue references (&&), std - move, std - forward

Step 4 — && is the sign that says "this one is stealable"

WHAT. We introduced a new type, T&&, whose superpower is that it refuses to bind to named survivors and only accepts about-to-die temporaries.

WHY do we need a whole new type? So we can write two versions of a constructor: one taking const T& (for survivors → must copy) and one taking T&& (for ghosts → may steal). The compiler then auto-picks the stealing version whenever the source is a ghost. See Reference collapsing for how && behaves inside templates.

PICTURE. Two doors labelled const T& and T&&. The named object a can only fit through the copy door; the temporary ghost fits through the move door.

Figure — Move semantics — rvalue references (&&), std - move, std - forward

Step 5 — The move constructor: repoint the arrow, don't copy the box

WHAT. We transferred ownership of the big box by copying a single 8-byte address, then severed the source's link to it.

WHY the last two lines are not optional. After data = o.data, two arrows point at one box (Step 5a shows this danger). Nulling the source guarantees only one owner survives.

PICTURE. Frame 1: both arrows point at one box (dangerous). Frame 2: the source arrow is snipped and hangs loose at nullptr; only the destination owns the box now.

Figure — Move semantics — rvalue references (&&), std - move, std - forward

Step 5a — Degenerate case: what if we forgot to null the source?

WHAT. We deliberately broke Step 5 to see the disaster it prevents.

WHY include the failure? Ownership is the whole game (see RAII and Ownership). A moved-from object that still points at the box is a bomb with a timer set to "next destructor".

PICTURE. Both destructors reach for the one box; the second delete[] is drawn in red exploding — the double-free.

Figure — Move semantics — rvalue references (&&), std - move, std - forward

Step 6 — Edge case: the moved-from object is valid but empty

WHAT. We inspected the source after the move: it is empty, consistent, and harmless — but its old contents are gone.

WHY this is the safe contract. delete[] nullptr is defined to do nothing, so an empty moved-from object cleans up trivially. That is precisely why we chose nullptr/0 and not some junk value.

PICTURE. The source object drawn as two empty cells (nullptr, 0) with a green "safe to destroy or reassign" tag and a red "do not read old value" tag.

Figure — Move semantics — rvalue references (&&), std - move, std - forward

Step 7 — std::move is a cast, not an action

WHAT. std::move does not move a single byte. It simply retypes a survivor as a ghost so the move door opens.

WHY we need it. A named variable a is an lvalue even when you are truly done with it. std::move(a) is your promise: "I won't read a again — treat it as stealable." The actual stealing still happens inside Step 5's constructor.

PICTURE. std::move drawn as a rubber stamp changing a's label from "lvalue / keep" to "rvalue / stealable"; the box underneath is untouched.

Figure — Move semantics — rvalue references (&&), std - move, std - forward

The one-picture summary

Figure — Move semantics — rvalue references (&&), std - move, std - forward
Recall Feynman: tell it to a 12-year-old

Imagine your stuff lives in a giant crate, and your pocket holds a note with the crate's address. Copying = buy an identical empty crate and re-pack a duplicate of everything — slow, one item at a time. Moving = just hand over your note. Now your friend's note points at the crate, and you scribble out your own note so you don't both try to throw the crate away later. && is a doorway that only lets in stuff that's about to be thrown out anyway — safe to plunder. std::move isn't a truck; it's a sticker you slap on your own crate that says "you can take this," which sends it through that doorway. And if you forget to scribble out your old note, one day you and your friend both try to trash the same crate — that's the crash we call a double-free.

Recall Quick self-test

Why is a Str&& parameter still an lvalue inside the function? ::: Because inside the function it has a name, and named things are lvalues — you must std::move it again to move from it. What state is a moved-from object left in? ::: Valid but unspecified — here data = nullptr, len = 0; safe to destroy or reassign, unsafe to read. Does std::move move data? ::: No — it is static_cast<T&&>, a compile-time relabel with zero runtime cost.


Related: Copy elision and RVO · std::vector internals · noexcept specifier · Templates and type deduction