Worked examples — Move semantics — rvalue references (&&), std - move, std - forward
Parent: Move Semantics topic note
This page is a drill through every case move semantics can throw at you. First we lay out a matrix of every scenario class — every value-category, every degenerate object, every "gotcha" — then we work each cell fully. If a case exists, you will see it here.
Before we start, one anchor you must never lose:
If any symbol below feels unfamiliar, it is defined the moment it enters. Let's build the map first.
The scenario matrix
Think of every move-semantics situation as landing in one of these cells. The columns are "what kind of thing are we handing over?" and the rows are "what machinery is involved?".
Every worked example below is tagged with the cell(s) it hits. Together they cover the whole table.
Here is the mental picture that unifies all of them — the difference between copy (build a whole new buffer) and move (re-point the pointer, empty the source):

Look at the two panels. On the left, copy allocates a fresh box and duplicates every byte. On the right, move just redraws the arrow from destination to the existing box and blanks the source's arrow. That single picture is what all ten cells are really about.
Example 1 — Cell A: named lvalue copies by default
Forecast: guess before reading — copy or move? How many
new[]calls?
- Identify the value category of
a.ais a variable — it has a name and an address. Why this step? Selection between copy and move is decided purely by whether the argument is an lvalue or rvalue. A name ⇒ lvalue. - Match against the binding rules. An lvalue cannot bind to
Str&&(rvalue ref), but binds happily toconst Str&. Why this step? The compiler picks the only viable overload — the copy constructorStr(const Str&). - Count the work. The copy constructor runs
new char[len+1]once, thenmemcpy. So 1 allocation forb. Why this step? We're auditing cost; copy = allocate + byte-copy.
Verify:
ais untouched (still"hello", length 5),bis an independent"hello". Two separate buffers, one allocation forb. ✅
Example 2 — Cell B & Cell D: std::move on a named lvalue, then reading it
Forecast: does
std::move(a)copy or move? What happens toa?
- Apply the cast.
std::move(a)isstatic_cast<Str&&>(a)— it relabelsaas an rvalue. Why this step? Now the argument is an rvalue, so it binds toStr(Str&&): the move constructor is chosen. - Execute the move body.
c.data = a.data; c.len = a.len;thena.data = nullptr; a.len = 0;. Why this step? Two objects must never own the same buffer, so the source is emptied to a valid but unspecified state. - Answer (ii). After the move,
a.len == 0anda.data == nullptr. Readinga's value as if it still said"hello"is a Cell D mistake — the standard only guarantees it's valid (safe to destroy or reassign), not meaningful. Why this step? The whole point: a moved-from object is a hollow shell.
Verify:
cholds"hello"(len 5),a.len == 0. Zero new allocations forc— the buffer was transferred, not duplicated. ✅
Example 3 — Cell C: pure temporary auto-moves, no std::move
Forecast: copy, move, or neither?
- Classify
makeStr(). A function returningStrby value produces a prvalue (a pure temporary with no name). Why this step? No name ⇒ rvalue ⇒ binds toStr&&naturally. - Realise
std::moveis redundant here. WritingStr d = std::move(makeStr());would cast an already-rvalue to an rvalue — no benefit, and it can block copy elision (see Example 7). Why this step?std::moveis for turning lvalues into rvalues; a temporary is already one. - What actually happens. With guaranteed copy elision (C++17), the temporary is constructed directly into
d— zero constructor calls, not even a move. Why this step? The compiler skips the middleman entirely.
Verify:
d == "hello", and in C++17 exactly 0 move/copy constructor calls (elided). Addingstd::movewould force 1 move call. ✅
Example 4 — Cell E: an Str&& parameter is an lvalue inside
Forecast: the type says
&&— surely it moves, right?
- Look at
sinside the body.sis a named parameter. A name ⇒ lvalue, regardless of its declared type. Why this step? Value category is about the expression, not the type.Str&&describes whatsmay bind to, not whatsis once named. - So
Str kept = s;copies. The lvaluesmatchesconst Str&. Why this step? This is the single most surprising rule — the&&in the type is a decoy inside the function. - Fix. Write
Str kept = std::move(s);. Now the cast turns the lvaluesback into an rvalue and the move ctor fires. Why this step? You knowswas handed to you as stealable, so you re-assert that withstd::move.
Verify: With
std::move(s): 0 allocations,semptied. Without it: 1 allocation (a wasteful copy). ✅
Example 5 — Cell F: perfect forwarding preserves category
Forecast: for each call, does
calleecopy or move?
- Deduce
Tfor each call.T&&here is a forwarding reference (aT&&whereTis deduced). Forwrapper(x)(lvalue),T = Str&; forwrapper(makeStr())(rvalue),T = Str. Why this step? The deducedTencodes how the caller passed the argument. - Apply reference collapsing to
std::forward<T>(arg)=static_cast<T&&>(arg). Using the rules and :T = Str&⇒Str& &&collapses toStr&⇒ forwarded as lvalue ⇒calleecopies.T = Str⇒Str&&⇒ forwarded as rvalue ⇒calleemoves. Why this step? Reference collapsing is the machinery that turns the rememberedTback into the right category.
- Contrast with the buggy
callee(arg). Plainargis a name ⇒ always lvalue ⇒ always copies (Cell E again).std::forwardis what rescues the rvalue case. Why this step? Shows why forward exists.
The collapsing table, drawn:

Verify:
wrapper(x)→ 1 copy (allocation),xintact.wrapper(makeStr())→ 1 move (0 allocations). ✅
Example 6 — Cell H: std::move on a no-move type silently copies
Forecast: we wrote
std::move— does it move?
- Check what overloads exist. Declaring a copy constructor suppresses the implicitly-generated move constructor. So
Plainhas only a copy ctor. Why this step?std::moveonly offers an rvalue; something must exist to accept it as a move. - Overload resolution. The rvalue
std::move(a)has noPlain&&overload to bind to, butconst Plain&accepts rvalues too. Why this step?const T&is the universal fallback (it binds to both lvalues and rvalues). - Result: the copy constructor runs.
std::movedid nothing observable except waste your intent. Why this step? This is the "std::movemakes it fast by itself — false" lesson, made concrete.
Verify:
a.vis unchanged after the line (copy leaves source intact);b.vis an independent duplicate. If a move had happened,a.vwould be empty. ✅
Example 7 — Cell G: return std::move(x) is a pessimization
Forecast: intuition says
std::moveon return avoids a copy — is that right?
- Analyse (A)
return x;. For a local variable returned by value, the compiler applies NRVO (named return value optimization) — it constructsxdirectly in the caller's slot: 0 copies, 0 moves. Why this step? Elision beats even a move. - Analyse (B)
return std::move(x);.std::move(x)yields an expression that is not the name of a local variable — it's a cast result. This makesxineligible for NRVO. The compiler must now perform a move into the return slot: 1 move. Why this step? By writingstd::move, you traded a free elision for a cheap-but-nonzero move. - Verdict. (A) is faster (or equal). Rule:
return x;— let elision win. Why this step? Cements the mistake pattern.
Verify: (A) → 0 move ctor calls; (B) → 1 move ctor call. So calls_A < calls_B, i.e. (A) is at least as cheap. ✅
Example 8 — Cell I: why noexcept gates vector growth
Forecast: guess the allocation count and the move count.
- Allocate the new buffer. Growing needs 1 big allocation for the capacity-8 array. Why this step?
vectornever resizes in place; it relocates. - Relocate the 4 existing elements. Because the move ctor is
noexcept,vectoruses it: 4 element moves (each move = 0 per-element heap allocations, just pointer steals). Why this step? If the move ctor could throw, a half-moved buffer couldn't be rolled back, sovectorwould copy instead (4 element copies = 4 extra allocations).noexceptis what unlocks the cheap path. - Tally. 1 buffer allocation + 4 pointer-stealing moves + 0 per-element allocations. Had it been throwing-move: 1 buffer allocation + 4 element copies = 5 allocations total. Why this step? Makes the
noexceptpayoff a number.
Verify: noexcept path total allocations = 1; throwing path total allocations = 5. Element moves in noexcept path = 4. ✅
See also std::vector internals and noexcept specifier for the reallocation machinery.
Example 9 — Cell J: self-move safety in copy-and-swap assignment
Forecast: self-assignment often breaks naive
operator=— does this one survive?
- Build the by-value parameter
o.std::move(x)is an rvalue, soois move-constructed fromx:o.p = x.p, thenx.p = nullptr. Why this step? The by-value parameter is a separate object;x's guts are now insideo, andxis a valid-but-empty shell. - Swap.
std::swap(p, o.p)exchangesx.p(nownullptr) witho.p(the real buffer). After the swapx.pholds the real buffer again,o.pholdsnullptr. Why this step? The swap is symmetric and never aliases the same pointer twice. - Destroy
o. At function endo's destructor runsdelete[] o.pwhereo.p == nullptr— a no-op.xretains the live buffer. Why this step? No pointer is freed twice; self-move is safe for free.
Verify: After
x = std::move(x),xstill owns its original buffer (non-null), anddelete[]was called onnullptronly. Number of double-frees = 0. ✅
Example 10 — Word problem: a document editor's undo buffer
Forecast: guess the MB copied for each strategy.
- Copy strategy cost.
push_back(current)copiescurrent— 1 MB ofmemcpyper edit. Over 100 edits: MB copied. Why this step? Each copy duplicates the whole buffer bytewise. - Move strategy cost.
push_back(std::move(current))steals the pointer — 0 bytes copied per push. Over 100 edits: MB copied. Why this step? A move is pointer arithmetic, independent of buffer size. - Interpret. Move saves the entire 100 MB of copying. This is exactly the RAII and Ownership transfer idea in action. Why this step? Connects the number to the real-world win.
Verify: copy strategy = 100 MB copied; move strategy = 0 MB copied; savings = 100 MB. ✅
Recall Self-test: name the cell
Str b = a; where a is a named variable — copy or move? ::: Copy (Cell A: named lvalue).
Str c = std::move(a); — what state is a left in? ::: Valid but unspecified; a.len == 0, a.data == nullptr (Cell B/D).
Inside void f(Str&& s), is s an lvalue or rvalue? ::: An lvalue — it has a name (Cell E). Use std::move(s) to move.
Why is return std::move(x); slower than return x;? ::: It disables NRVO/copy elision, forcing an actual move (Cell G).
std::move on a type with only a copy ctor does what? ::: Silently copies — no move overload to bind to (Cell H).
See also: Rule of Five, Reference collapsing, Templates and type deduction, Copy elision and RVO.