5.2.9 · D5C++ Programming

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

1,810 words8 min readBack to topic

Before you start, hold four pictures in mind — look at the figure below:

Figure — Move semantics — rvalue references (&&), std - move, std - forward
  • An lvalue is a box with a name on it (persists, has an address).
  • An rvalue is a floating parcel with no name (a temporary, about to expire).
  • Moving = re-taping the shipping label onto a new box (steal the pointer); copying = manufacturing an identical box (deep copy).
  • std::move and std::forward are just labels you slap on — they move nothing by themselves; they only change which overload the compiler picks.

A jargon glossary you must have before the questions

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

True or false — justify

std::move(x) performs the actual move of x's resources.
False. It is a static_cast<T&&>(x) with zero runtime cost; the real stealing happens later inside whatever move constructor/assignment gets selected because of the cast.
An rvalue reference variable, once it has a name, is itself an lvalue inside the function.
True. void f(Str&& o) — the parameter o has a name and an address, so o is an lvalue; you must std::move(o) again to move from it.
std::move on an object whose class has no move constructor is a compile error.
False. It silently binds to the copy constructor (via const T&), so you get a copy with no warning — a common silent performance loss.
return std::move(localVar); is a good habit for avoiding copies.
False. For a local it disables NRVO (Named Return Value Optimization) and forces a move (or worse), often slower; plain return localVar; already elides or implicitly moves.
A const T& reference can bind to both an lvalue and an rvalue.
True. const T& accepts named things and temporaries alike, which is exactly why it was the pre-C++11 "one overload catches all" copy path.
A moved-from object is left in an undefined state and using it is undefined behaviour.
False (subtly). It is left in a valid but unspecified state — destroying it or assigning to it is fully safe; only reading its value expecting the old data is meaningless.
Inside a template, T&& always means "rvalue reference".
False. When T is a deduced parameter, T&& is a forwarding (universal) reference that binds to both lvalues and rvalues; only when T is fixed (Str&&) is it a plain rvalue reference.
Forgetting noexcept on a move constructor only affects documentation, not behaviour.
False. std::vector reallocation uses your move constructor only if it is noexcept; without it, the vector falls back to copying to keep strong exception safety — see std::vector internals and noexcept specifier.
std::forward<T>(arg) and std::move(arg) do the same cast when the argument is an rvalue.
True in that specific case. When T deduces to a non-reference, std::forward<T> collapses to an rvalue cast identical to std::move; the difference is forward is conditional on T, move is unconditional.
Copy elision means the copy/move constructor is guaranteed never to run on a returned temporary.
Partly true. Since C++17 elision is mandatory for prvalues (a temporary constructed directly into the return slot); for NRVO of a named local it is permitted but not guaranteed — see Copy elision and RVO.

Spot the error

template<class T> void wrap(T&& a){ callee(a); }
``` ::: Bug: `a` has a name, so `callee(a)` always passes an lvalue and copies even when a temporary was given. Fix: `callee(std::forward<T>(a));`.
 
```cpp
Str(Str&& o) noexcept : data(o.data), len(o.len) {}
``` ::: Missing `o.data = nullptr; o.len = 0;`. Both objects now own the same buffer; two destructors `delete[]` it twice → double-free UB. Reset the source to empty-but-valid.
 
```cpp
Str c = std::move(a);
std::cout << a;   // print old contents
``` ::: Reading a moved-from object's value: after the move `a`'s value is unspecified, so this prints garbage. Only reassign or destroy `a` afterward.
 
```cpp
void take(std::string&& s){ store(s); }
``` ::: Inside `take`, the named `s` is an lvalue, so `store(s)` copies. To actually move, write `store(std::move(s))`.
 
```cpp
template<class T> void log(T&& x){ sink(std::move(x)); }
``` ::: Using `std::move` on a forwarding reference unconditionally strips lvalue-ness: an lvalue caller gets its object silently plundered. Use `std::forward<T>(x)` so lvalues stay copies.
 
```cpp
void concrete(Widget&& w){ use(std::forward<Widget>(w)); }
``` ::: `std::forward` misused: `Widget&&` here is a *fixed* rvalue reference, not a deduced forwarding reference, so there is no value category to restore. Use `std::move(w)` instead.
 
```cpp
Str& operator=(Str&& o){ delete[] data; data=o.data; len=o.len; return *this; }
``` ::: No self-move guard and no reset of `o`. On self-assignment `delete[] data` then reading `o.data` reads freed memory; also `o` still owns the buffer. The copy-and-swap `operator=(Str o)` from the parent avoids both problems (see [[Rule of Five]]).
 
---
 
## Why questions
 
Why does overloading on `const T&` versus `T&&` let the compiler auto-select copy vs move? ::: Because a named lvalue can only bind to `const T&`, while a temporary rvalue prefers the better-matching `T&&`; the binding rules route "named thing → copy" and "temporary → move" automatically.
 
Why must a moved-from object be *valid* and not merely emptied? ::: Its destructor still runs, and other code may assign to it; leaving it in a coherent state (e.g. `nullptr`, `len=0`) guarantees the destructor and reassignment behave, satisfying RAII (Resource Acquisition Is Initialization) — see [[RAII and Ownership]].
 
Why does `std::forward` need the template parameter `T` written explicitly as `std::forward<T>(x)`? ::: `T` encodes how the caller passed the argument (deduced to `U&` for lvalues, `U` for rvalues); `forward` reads that `T` to pick the right cast, so it cannot deduce it from `x` alone (`x` is always an lvalue inside).
 
Why does reference collapsing exist at all? ::: Deduction can produce "reference to reference" like `U& &&`; the collapsing rules ($T\&\ \&\& \to T\&$, $T\&\&\ \&\& \to T\&\&$) turn those into a single reference so forwarding references have well-defined types — see [[Reference collapsing]].
 
Why is taking the assignment parameter *by value* (`operator=(Buf o)`) elegant? ::: The by-value parameter is built by copy or move as appropriate, then a cheap `swap` transfers guts and the parameter's destructor frees the old data — one function covers copy- and move-assignment and is self-assignment safe.
 
Why does `std::move` on a `const` object fall back to copying? ::: `std::move(constObj)` yields a `const T&&`, which cannot bind to the non-const `T&&` move constructor, so overload resolution picks `const T&` (copy). Declaring things `const` quietly forbids moving them.
 
---
 
## Edge cases
 
What does `std::move` do to a fundamental type like `int`? ::: Nothing useful — `int` has no heap to steal, so the "move" is just a copy of the bits; `std::move` is harmless but pointless here.
 
Calling `wrapper(makeStr())` where `T` deduces to a non-reference — what value category does `std::forward` produce? ::: An rvalue. `T` is a plain type, so `static_cast<T&&>` is an rvalue-ref cast, letting `callee` select the move overload.
 
Calling `wrapper(x)` with a named `x` — what does `T` deduce to and what is forwarded? ::: `T` deduces to `U&`; reference collapsing makes `T&&` into `U&`, so `std::forward` produces an lvalue and `callee` copies — exactly the caller's intent.
 
What happens if a move constructor throws while `std::vector` is reallocating a `noexcept`-less type? ::: The vector never risks it — because the move is not `noexcept`, it copies elements instead, so a throw mid-way leaves the original vector intact (strong guarantee) at the cost of copy performance.
 
Is `Str c = std::move(a);` legal if `Str` has a deleted move constructor but a valid copy constructor? ::: It depends: an explicitly *deleted* move constructor participates in overload resolution and wins for the rvalue, making it a compile error; a merely *absent* move constructor lets the copy constructor be chosen instead.
 
What is the value category of the temporary returned by `std::move(x)`? ::: An xvalue (an "expiring" rvalue, defined in the glossary above) — it names an object whose resources may be reused, which is precisely what lets it bind to `T&&`.
 
Can you move from a `const std::string&&`? ::: No — `const` blocks it. The move constructor needs a non-const source to null out its pointer, so a `const` rvalue matches only the copy constructor.
 
---
 
> [!recall]- One-line self-test
> If a "move" gave you no speedup, name the three usual culprits.
> Culprits ::: No move constructor exists (silent copy); the move constructor isn't `noexcept` (vector copies); or you wrote `std::move`/`std::forward` on the wrong thing (const object, named `&&` param, or elision you accidentally disabled).