The one problem
A function wants to know one thing: am I allowed to gut this argument? Stealing a buffer instead of copying it is an enormous win, but it is only safe when nobody will look at the source again. The caller is the only one who knows that. The function has no way to find out on its own.
So the information has to travel from the call site into the function. C++ sends it through the type system, at compile time, at zero runtime cost. Every rule in this document is a consequence of that one decision.
1. The caller tags the argument. That tag is its value category.
s lvalueSomeone still holds it.
std::move(s) xvalueHeld, but the caller is done with it.
make() prvalueNo object yet.
glvalue and rvalue are group names, not extra kinds: glvalue is lvalue plus xvalue, rvalue is xvalue plus prvalue. Ignore them until you are reading the standard.
2. The function declares its intent. Four reference types, four intentions:
const T&I will only read it.
T&I may modify your object.
T&&I intend to gut it.
const T&&I refuse anything you still hold. Exists to be = deleted.
3. Binding matches the two. Two lines cover the whole grid in section 3:
const T&takes anything. A reader can hurt nobody.T&&refuses lvalues. That refusal is the feature: accepting a name the caller still holds would mean gutting a live object.
The rest follows. A temporary binds to const T& and T&& because nobody else holds it. const blocks T&& because you cannot steal from what you promised not to modify.
Why the two famous rules stop being weird
"A named rvalue reference is an lvalue." Permission is on the expression, not the object. If the name carried it, line 2 would already have gutted x and line 3 would find wreckage.
void f(std::string&& x) {
g(x); // x has a name, so this expression is an lvalue: g copies
g(std::move(x)); // re-assert the permission on the last use: g moves
}
"std::move does not move." It is a cast from the lvalue tag to the xvalue tag and compiles to no instructions. Whether a move happens is decided afterwards, by which overload that tag selects.
Two things here are badly designed, so being confused by them is not a comprehension failure: T&& means two unrelated things depending on whether T is deduced, and std::move is misnamed.
The three categories fall out of two yes/no questions. Does the expression name something with an identity, an address you could take? And is the compiler allowed to treat its insides as stealable? Tap an expression.
Two things worth pinning down now. A prvalue has no identity because, since C++17, there is no object there yet: a prvalue is a recipe for initialising one. And an xvalue is not a different kind of object, it is an ordinary object you have promised not to look at again.
1What the two words actually mean
Identity is about whether the expression finds an object that already exists, or produces one. Ask "which object is this?" and see whether the question has an answer. Tap each expression and watch where it points.
std::string s = "hello, world"; std::string& r = s; auto* p = &s; std::string make();
The first four expressions are four different spellings of the same object. That is what identity buys you: two expressions can denote one object, and you can ask whether they are the same. A prvalue has nothing to compare, because until something demands a real object, none exists.
Careful with the shortcut "identity means you can take its address". Unary & requires an lvalue, so &std::move(s) is ill-formed even though the xvalue plainly denotes an object. The honest test is the one above: can another expression denote the same object.
Stealable does not mean the object changes owners. The object stays exactly where it is, at the same address, alive until its scope ends. What moves is what it owns: the heap pointer, the file descriptor, the buffer. Run both and compare.
Notice what the move did not do. No allocation, not one character copied, and both stack objects stayed at their own addresses the whole time. Only the pointer inside changed hands, which is why a move is cheap and why the source is left empty rather than destroyed.
One more thing the category does not tell you: whether a move is even possible. std::move(i) on an int is still an xvalue, and so is std::move(x) for a type whose move constructor is deleted. The category is a permission, not a capability. It means the language will let a callee assume nobody looks at this object again; what the callee does with that permission is a separate question, settled by overload resolution.
| object exists already | nobody will look again | |
|---|---|---|
| lvalue | yes | no, keep it intact |
| xvalue | yes | yes, gut it |
| prvalue | no | nothing to protect |
The fourth combination is missing for a reason. If no object exists, there is nothing to preserve, so "no identity and not movable" cannot occur. Three questions' worth of answers, three categories.
2Type and category are different axes
If an expression's type would be a reference, that reference is stripped off and shows up in the category instead. So no expression ever has reference type, and the single most common bug in hand-written move code follows directly: a parameter declared std::string&& is, when you use its name, an lvalue.
- expression
- tap a highlighted expression
- type
- category
- binds to
T&&
Read the rule out loud, because interviewers ask for it in exactly these words: a named rvalue reference is an lvalue. If it were not, every use of a parameter inside a move constructor would silently gut it, and you could never use the parameter twice.
3What binds to what
Four reference types, four kinds of argument. This grid is the whole of reference binding; everything later in the document is a consequence of it. Tap any cell.
| argument | T& | const T& | T&& | const T&& |
|---|
const T& is the only reference that binds all four. That single property is why it was the default parameter type for everything before C++11, and it is also why move semantics needed a new reference type rather than a new rule: there was no way to say "only bind if it is safe to steal".
MSVC historically allowed binding a non-const lvalue reference to an rvalue as an extension. It is ill-formed in standard C++, and /permissive- rejects it.
4Overload resolution playground
Binding says what is possible. Overload resolution picks among the possibilities, and only two tiebreakers matter here: a less const-qualified reference beats a more const-qualified one, and for an rvalue argument, binding an rvalue reference beats binding an lvalue reference. Toggle overloads and watch every call site update.
| call | argument is | selected |
|---|
const T&, the C++03 world, then add T&&.Things worth trying. Enable only f(T) and one reference overload: every call becomes ambiguous, because passing by value and binding a reference are both exact matches and nothing breaks the tie. Then look at the last row: std::move on a const object produces a const rvalue, which T&& cannot bind, so it quietly lands on const T& and copies.
Notice also that f(std::move(s)) and f(make()) always agree. An xvalue and a prvalue are interchangeable for overload resolution; the difference between them is about whether an object exists yet, not about who wins.
5Temporaries and the extension rule
A prvalue is an expression, not an object. make() does not denote a string; it is closer to an instruction for producing one. An object appears only when something in the surrounding code needs one to exist, and binding a reference is one of those things. That step is temporary materialisation, and the object it produces is the temporary.
Binding a reference to a temporary can extend the temporary's life to match the reference. The rule is narrower than people remember: the binding must be direct. Step through four initialisers and watch where each reference actually ends up pointing.
Cases one and two start identically and diverge only at the arrow. Extension is a property of the binding, not of the object. Land the arrow on the temporary and it lives; land it on a function's return value and nothing is holding the temporary, so it dies at the semicolon. The reference points at the same bytes either way.
The routes that count as direct are a fixed list:
| Route | Extends | Example |
|---|---|---|
| straight to the prvalue | yes | const S& r = make(); |
| parentheses | yes | (make()) |
. member access | yes | Point{1,2}.x |
| raw array subscript | yes | makeArr().a[0] |
| named cast | yes | static_cast<S&&>(make()) |
| any function call | no | std::move(make()), make().name(), v[0] |
| reference member in a ctor init list | no | Holder() : r(make()) {} |
Two traps. operator[] on a class is a function call, so makeVector()[0] dangles while makeStruct().rawArray[0] does not. And std::move is that static_cast, yet only one row extends: what counts is whether the cast is in your source or inside a callee. On a prvalue the std::move is pointless anyway. If it has no name, it needs no std::move.
Now predict
Decide first, then watch the two bars: amber is the temporary, teal is the reference.
Why the rule is shaped like this
Extension is decided entirely at compile time. There is no refcount, no ownership flag on the object, nothing at runtime tracking whether a temporary is being held. The compiler reads the initialiser, decides syntactically whether this is an extending binding, and then emits the destructor call at a different point in the generated code. That is the whole feature, and it costs nothing.
Which is also why it stops at function calls. To know that a returned reference points back at an argument, the compiler would have to open up the callee, and in general it cannot: the body may be in another translation unit, and the answer may not even be fixed at compile time.
const T& pick(const T& a, const T& b) { return cond ? a : b; }
const T& r = pick(make(), make()); // which temporary should be extended?
There is no compile-time fact to act on, so the standard draws the only line it can draw locally. For the same reason extension cannot be passed along. A function can never hand back an extended temporary:
const std::string& bad() {
const std::string& r = make(); // extended, to bad()'s scope
return r; // caller gets a reference to a dead object
}
The extension inside bad was real, and it was tied to r. When r dies at the closing brace the temporary goes with it. Extension never crosses a function boundary in either direction.
The habit
The through-a-function cases are the dangerous ones because they look identical at the call site. p.name() and makePerson().name() are the same call on the same function, and the declaration of name() tells you nothing about which is safe. A useful habit: treat any function returning const T& as returning a borrow whose owner you must be able to name. Named local, static, or one of the arguments you passed are fine. If the owner is a temporary in the same expression, take a copy instead.
This is the trap in std::min, std::max and std::clamp, all of which return a reference to a parameter, so const int& m = std::min(a, 5); may outlive the 5. Default to auto rather than auto&& for anything coming out of a function, and turn on -Wdangling-reference on GCC or -Wdangling on Clang. When you write such a function yourself, you can make the dangerous call fail to compile:
const std::string& name() const& { return name_; }
const std::string& name() const&& = delete; // .name() on a temporary: error
If you have written Rust, this is what a lifetime annotation encodes. fn name(&self) -> &String ties the borrow to self and the borrow checker refuses to let it outlive the owner. C++ has the same underlying concept with no checker, so naming the owner is the manual version of that analysis.
6Forwarding references
T&& is an rvalue reference only when T is fixed. When T is deduced in that exact position it accepts anything and records which it got inside T, so one wrapper replaces the 2n overloads you would otherwise write.
template <class T>
void relay(T&& x) { // forwarding reference: T is deduced here
sink(std::forward<T>(x)); // pass it on with the tag it arrived with
}
It belongs on a layer whose job is to pass things on, not to use them. Choose an argument, then choose what that layer writes.
Only the forward column is right in all three rows, which is the whole argument for the function existing. move steals from a caller who never offered; writing nothing downgrades a move to a copy, because x has a name and a name is an lvalue.
The mechanism: deduction puts an & into T for lvalues, and collapsing folds & && back to &, so an lvalue reference anywhere in the stack wins. std::forward<T>(x) is static_cast<T&&>(x), which collapses the same way and reproduces whatever arrived.
Two rules of use. Like move, forward is a one-shot permission slip, so forward on the last use only. And every layer in the chain must forward, since one const T& anywhere loses the category for good.
These are not forwarding references, and each one catches people out:
// T is deduced, but not in the T&& position template <class T> void f(std::vector<T>&& v); // const kills it: only binds rvalues template <class T> void g(const T&& x); // T belongs to the class, the call deduces nothing template <class T> struct vector { void push_back(T&& v); }; // this one is: auto&& deduces exactly like T&& auto&& r = anything();
That third case is why emplace_back carries its own Args&&...: T is fixed by the time you call a member, so a member alone can never forward. Same shape in make_unique and invoke, which exist to hand your arguments to a constructor untouched.
7Did anything actually move?
std::move moves nothing. It is a cast to an xvalue, it compiles to no instructions, and whether a move happens is decided afterwards by overload resolution. Run each line and watch the ledger.
The summary that survives the interview: std::move is an unconditional cast that asks for a move, std::forward is a conditional cast that preserves what it was given, and neither one guarantees a move happens. A const object or a const T& parameter on the receiving end silently turns the request into a copy.
8Where the temporaries went
Before C++17 a prvalue was a temporary object, and skipping its copy was an optional optimisation. Since C++17 a prvalue is not an object at all: it is an initialiser, and an object only appears when something needs one. That step is called temporary materialisation.
C++14 model
Up to three objects on paper. Compilers elided them, but a deleted move constructor still made the code ill-formed.
C++17 model
One object, guaranteed. The prvalue travels out of make() and initialises s directly, so a type with no copy and no move constructor can still be returned by value.
Materialisation is what happens when a prvalue meets something that needs a real object: binding it to a reference, accessing a member, or taking its address. At that moment it becomes an xvalue, which is why Point{1,2}.x is an xvalue and not a prvalue.
std::string make(); std::string s = make(); // C++17: zero copies, zero moves, guaranteed const std::string& r = make(); // prvalue materialises, then r extends it
9Check yourself
Answer out loud before opening each one.