C++ / MOVE SEMANTICS AND SPECIAL MEMBER FUNCTIONS
Rvalue references and binding to temporaries
Use T&& to accept only rvalues, predict which reference overload an argument binds to, and explain why a named rvalue reference is itself an lvalue.
What you will learn
- Declare T&& to accept only rvalues, while const T& keeps accepting everything
- Predict whether an argument binds to T&, const T&, or T&& and say why
- Explain why the expression naming an rvalue reference is an lvalue
- Extend a temporary's lifetime by binding it to a reference variable
Understanding Rvalue references and binding to temporaries
The && declarator creates a reference that binds only to rvalues: prvalues such as the result of make() or std::string{"x"}, and xvalues such as an object explicitly marked as expiring. Before C++11 a temporary could only be caught by const T&, which pinned it as read-only, so a function had no way to say "this argument is about to be destroyed, feel free to gut it". An rvalue reference parameter is exactly that statement encoded in the type system: a mutable reference the compiler hands you only when the caller has no further use for the object.
Three reference types divide up the value categories. T& accepts only non-const lvalues, const T& accepts everything, and T&& accepts only non-const rvalues. When both const T& and T&& are candidates, an rvalue argument prefers T&& because that binding needs no added qualification, while an lvalue argument leaves const T& as the only viable candidate; that asymmetry is what lets one overload set copy from lvalues and steal from temporaries with nothing special written at the call site. Constness outranks rvalue-ness, so a const temporary falls through to const T& and gets copied.
The trap is conflating a variable's declared type with the value category of an expression that names it. std::string&& r = make(); declares r with rvalue reference type and extends the temporary's lifetime to r's scope, but the expression r is an lvalue: it has a name, an address, and a future. That is why a parameter declared T&& p behaves like an ordinary reference inside the body, and why passing p onward unchanged reaches the const T& overload rather than the T&& one; you have to state explicitly that you are finished with it before it counts as expiring again.
<iostream>
<string>
void probe(const std::string& s) { std::cout << "const lvalue ref: " << s << '\n'; }
void probe(std::string&& s) { std::cout << "rvalue ref: " << s << '\n'; }
std::string make() { return "temporary"; }
int main() {
std::string named = "named";
probe(named); // lvalue -> const std::string&
probe(make()); // prvalue -> std::string&&
probe(named + "!"); // prvalue -> std::string&&
std::string&& r = make(); // temporary now lives as long as r
std::cout << "r holds: " << r << '\n';
probe(r); // the expression r is an lvalue -> const std::string&
}
An rvalue reference is a mutable reference that binds only to expiring values, which is how overload resolution tells temporaries from lvalues, yet giving one a name makes that name an lvalue.
Worked examples
Lifetime extension made visible
Shows that an unbound temporary dies at the semicolon while one bound to an rvalue reference survives to the end of the block.
<iostream>
struct Noisy {
int id;
explicit Noisy(int i) : id(i) { std::cout << "ctor " << id << '\n'; }
~Noisy() { std::cout << "dtor " << id << '\n'; }
};
int main() {
{
std::cout << "-- unbound temporary --\n";
Noisy{1};
std::cout << "after statement\n";
}
{
std::cout << "-- bound to rvalue ref --\n";
Noisy&& r = Noisy{2};
std::cout << "still alive: " << r.id << '\n';
}
std::cout << "done\n";
}
Example explained
Line 1Noisy{1}; creates a prvalue nothing binds to, so it is destroyed at the end of that statement, before "after statement" prints.
Line 2Noisy&& r = Noisy{2}; binds the temporary to a reference variable, which extends its lifetime to the end of r's block.
Line 3dtor 2 therefore prints at the closing brace, not after the initialising statement.
Line 4A const Noisy& would extend the lifetime identically; what Noisy&& adds is non-const access plus a refusal to bind lvalues.
Which reference wins
Resolves the same call against T&, const T& and T&& overloads for four different argument expressions.
<iostream>
<string>
void which(std::string&) { std::cout << "T&\n"; }
void which(const std::string&) { std::cout << "const T&\n"; }
void which(std::string&&) { std::cout << "T&&\n"; }
const std::string frozen() { return "c"; }
int main() {
std::string a = "a";
const std::string b = "b";
which(a); // non-const lvalue
which(b); // const lvalue
which(std::string{"t"}); // non-const prvalue
which(frozen()); // const prvalue
}
Example explained
Line 1which(a) picks std::string& because a non-const lvalue matches the plain lvalue reference exactly.
Line 2which(b) cannot use std::string& (that would drop const) and cannot use std::string&& (b is an lvalue), leaving const std::string&.
Line 3For std::string{"t"} both const std::string& and std::string&& are viable, and the rvalue reference is the better match.
Line 4frozen() yields a const prvalue, which std::string&& cannot bind to, so a const temporary lands on the copying overload.
Important notes
In a deduced context, template <class T> void g(T&& x), the && is a forwarding reference and can bind lvalues too; the rvalue-only rule here applies to concrete types such as std::string&&.
A temporary bound to a reference parameter only lives until the end of the full expression containing the call, so storing or returning that reference dangles even though the binding itself was legal.
Common mistakes
Treating a T&& p parameter as an rvalue inside the body: a nested call sink(p) selects the const T& overload and silently copies instead of moving.
Returning an rvalue reference to a local or temporary, as in std::string&& f() { return std::string{"x"}; } — lifetime extension does not survive a return, so the caller reads a destroyed object.
Writing std::string&& r = named; and expecting it to compile; an rvalue reference refuses to bind to an lvalue, and the build stops with "cannot bind rvalue reference to lvalue".
Try it yourself
Change, predict, then run
In a browser editor, define report(const std::vector<int>&) and report(std::vector<int>&&) that print the overload name and v.size(), then call report with std::vector<int>{1,2,3}, with a named vector, and with a named std::vector<int>&& bound to a temporary. Write down your prediction for all three lines before running it.
Open the C++ workspaceCheck your understanding
With overloads f(const std::string&) and f(std::string&&) available, and std::string&& r = std::string{"hi"}; in scope, which overload does f(r) call?
- f(std::string&&), because r is declared with rvalue reference type
- f(const std::string&), because the expression r is an lvalue regardless of r's declared type
- Neither; the call is ambiguous since both overloads are viable for r
- f(std::string&&), because r refers to a temporary that is about to expire
Show answer
Overload resolution looks at the value category of the argument expression, not the declared type of the variable, and a named variable is an lvalue, so only const std::string& is viable and the call is unambiguous. Option 3 is tempting but backwards: because the temporary was bound to r, its lifetime runs to the end of r's scope, so nothing is expiring and the language will not let you steal from it implicitly.