Understanding Rvalue References
Understand r-value references and when to use them instead of pointers.
What Are R-value References?
An r-value reference is a second kind of reference, spelled with two ampersands (int&&). It turns away every named object and accepts only expressions that own no name: literals, arithmetic results, and objects handed back by value from a function. C++11 added it so a function could answer one question through overload resolution alone: is the thing I was given about to be thrown away anyway? Every technique in this chapter, from move constructors to std::move, is built on top of that answer.
This lesson leans hard on value categories. If the split between an expression that names storage and one that merely produces a value has gone fuzzy, spend five minutes back in the value categories lesson before continuing.
Where const& Runs Out of Answers
Until C++11, a function that wanted to accept anything at all without paying for a copy had exactly one tool: a parameter of type const T&. It swallows a variable, a constant, a literal, or a return value, and it duplicates none of them. What it cannot do is report which of those it received.
That gap costs real work. Suppose a function receives a const std::string& that is bound to a temporary about to be destroyed at the semicolon. The characters in that string are seconds away from being released, and the function could have simply adopted the buffer instead of allocating a second one and copying into it. But nothing in the parameter's type distinguishes that temporary from a variable the caller still intends to use, so the safe assumption (treat the referent as somebody else's property) is the only assumption available.
What was missing was never a faster copy. It was a way to ask the question. && is how you ask it.
Declaring One
The declaration is the familiar reference syntax with the ampersand doubled:
int main()
{
int tally{74};
int& steady{tally}; // one ampersand: needs a named object
int&& fleeting{19}; // two ampersands: needs a nameless value
steady = fleeting; // once declared, each behaves as a plain int name
return 0;
}
The compiler decides purely from the initializer's value category. steady demands an expression that names storage; fleeting demands one that does not. Swap the two initializers and neither declaration survives, so the program below does not compile:
int main()
{
int tally{74};
const int ceiling{19};
int& borrowed{ceiling}; // rejected: a plain int& refuses a const target
int&& stolen{tally}; // rejected: && refuses a named object
return 0;
}
s.cpp: In function 'int main()':
s.cpp:6:26: error: binding reference of type 'int&' to 'const int' discards qualifiers
6 | int& borrowed{ceiling}; // rejected: a plain int& refuses a const target
| ^
s.cpp:7:23: error: cannot bind rvalue reference of type 'int&&' to lvalue of type 'int'
7 | int&& stolen{tally}; // rejected: && refuses a named object
| ^
Two diagnostics stating the same restriction from opposite ends.
One Grid Instead of Four Tables
Crossing const with & and && produces four reference forms. Read them off a single grid rather than memorizing them one form at a time. "Read/write" and "read only" describe what the reference permits, not what the underlying object permits.
| Initializer | int& |
const int& |
int&& |
const int&& |
|---|---|---|---|---|
Modifiable l-value (int tally) |
binds, read/write | binds, read only | rejected | rejected |
Non-modifiable l-value (const int ceiling) |
rejected | binds, read only | rejected | rejected |
R-value (19, a * b, a value returned by a function) |
rejected | binds, read only | binds, read/write | binds, read only |
Column one is the only reference the language had before C++11, and the grid shows how narrow it is: a modifiable l-value or nothing.
Column two is the universal acceptor. Anything in the left column will bind to a const int&, which is exactly why const T& became the default way to take a parameter cheaply, and it charges for that flexibility with a read-only view.
Column three inverts column one. int&& is the pickiest form of the four, rejecting everything except the nameless values, and it hands you write access to what it catches.
Column four, const int&&, is legal and close to useless. It accepts only the expendable values while forbidding you to touch them, which is the opposite of what you would want from a temporary. You will see it in the grid and almost nowhere else.
Writing Through an R-value Reference
The write access in column three is worth a demonstration, because the first version of it looks impossible:
#include <iostream>
int main()
{
int&& slot{41}; // a nameless int holding 41 springs into existence
slot = 88; // a non-const && lets us overwrite it
std::cout << slot << '\n';
return 0;
}
88
Nobody assigned a new value to the literal 41, which would be nonsense. A literal is not an object, so there is nothing for slot to attach to until the compiler materializes one: an unnamed int initialized to 41. slot is bound to that object, and since slot is not const, writing through it is ordinary assignment. The only thing being modified is a temporary that no other code in the program can reach.
Keeping a Temporary Alive
A temporary normally dies at the end of the full expression that produced it. Bind one directly to an r-value reference and its destruction is postponed until the reference itself goes away. A destructor that announces itself makes the postponement visible:
#include <iostream>
class Beacon
{
public:
explicit Beacon(int channel)
: m_channel{channel}
{
std::cout << "Beacon " << m_channel << " switched on\n";
}
~Beacon()
{
std::cout << "Beacon " << m_channel << " switched off\n";
}
int channel() const
{
return m_channel;
}
private:
int m_channel{};
};
int main()
{
std::cout << "top of block\n";
auto&& held{Beacon{74}};
std::cout << "channel " << held.channel() << " is usable\n";
std::cout << "bottom of block\n";
return 0;
}
top of block
Beacon 74 switched on
channel 74 is usable
bottom of block
Beacon 74 switched off
Without the extension, "switched off" would print on the line right after "switched on", and held.channel() would be reading a destroyed object. Instead the destructor waits for the closing brace, so calling through held is safe for the whole block. Writing Beacon&& held{Beacon{74}} behaves identically; auto&& just spares you from repeating the type.
Lifetime extension belongs to the act of binding directly to a temporary, not to the `&&` token. A `const` l-value reference extends a temporary in exactly the same way, and an `&&` bound to something that already has a name and a lifetime of its own extends nothing.
Choosing Between Overloads
Neither of the two previous examples is how r-value references earn their keep. Their real job is in a parameter list, where two overloads let one function name take two different routes:
#include <iostream>
void charge(const int& durable)
{
std::cout << "const& path: " << durable << '\n';
}
void charge(int&& fleeting)
{
std::cout << "&& path: " << fleeting << '\n';
}
int main()
{
int balance{74};
charge(balance); // balance owns a name
charge(19); // 19 owns nothing
return 0;
}
const& path: 74
&& path: 19
balance names an object, so the second overload is not even a candidate and the call goes to the const int& version. The literal 19 makes both overloads viable, and here the tie-break matters: binding an r-value to int&& outranks binding it to const int&, so the second overload wins.
That ranking is the whole mechanism. A single pair of overloads now knows, at compile time and at zero runtime cost, whether the caller's argument has a future. The next lesson spends that knowledge on move constructors, where the && overload stops copying the argument and starts dismantling it.
A Name Turns It Back Into an L-value
Now the trap that catches every C++ programmer exactly once:
#include <iostream>
void charge(const int& durable)
{
std::cout << "const& path: " << durable << '\n';
}
void charge(int&& fleeting)
{
std::cout << "&& path: " << fleeting << '\n';
}
int main()
{
int&& parked{19};
charge(parked); // parked owns a name
charge(static_cast<int&&>(parked)); // a cast strips that name away
return 0;
}
const& path: 19
&& path: 19
The first call does not take the && route. parked was declared int&&, but the moment you write parked in an expression you are naming an object, and naming an object yields an l-value. A declaration's type and an expression's value category answer different questions. parked has type int&&, yet the expression parked is an l-value, just as int tally has type int while tally is an l-value and the literal 74 is an r-value.
So charge(parked) is not merely a preference for the const int& overload. The int&& overload is excluded outright, because an r-value reference has nothing to bind to when handed a name.
The second call gets around it by casting the name back to an unnamed expression. static_cast<int&&>(parked) produces an expression with no identity of its own, which the && overload accepts. That cast is precisely what std::move performs, and a later lesson in this chapter gives it its proper name and its proper cautions.
Declaring something `int&&` constrains what it may be initialized from. It says nothing about how the name behaves afterwards. Inside a function, a parameter of type `T&&` is an l-value, so passing it onward by name hands the next function an l-value and quietly selects the copying overload.
Never Return T&&
A function whose return type is T&& gives the caller a reference to storage the function was keeping alive, and that storage is released as the function returns. What arrives at the call site is a dangling reference, and reading it is undefined behavior. The reasoning is identical for a return type of T&: the referent does not outlive the call that produced it.
Treat a return type of `T&&` as a defect until proven otherwise. Return by value instead. A local sent back by value is moved rather than copied wherever that is possible, and the call's result is itself an r-value, so the copy you were trying to dodge is usually not there to begin with.
Looking Forward
Overload resolution on && is the trigger; the payload comes next. Move constructors and move assignment operators use an && parameter to transfer a resource out of a doomed object rather than duplicate it, and std::move lets you opt into that path deliberately for an object you are finished with. Later in the chapter, std::unique_ptr shows a type built so that moving is the only way to hand it around.
Key Terminology
- R-value reference: a reference spelled
T&&, whose initializer must be an expression that owns no name - Lifetime extension: the rule that a temporary bound directly to a reference lives as long as the reference rather than dying at the end of its full expression
- Value category: the property of an expression that says whether it names an object (l-value) or merely produces a value (r-value)
- Overload resolution: the compile-time selection among candidate functions, which ranks an r-value's binding to
T&&above its binding toconst T& - Dangling reference: a reference to storage that has already been released
Summary
T&&declares an r-value reference, which catches nameless values and turns away every named objectT&is the mirror image, taking a modifiable l-value and nothing else- An l-value reference to const is the universal acceptor, binding to modifiable l-values, non-modifiable l-values, and r-values alike, always with a read-only view
- A non-const r-value reference allows writing to the temporary it is bound to, because the reference is attached to a materialized object rather than to a literal
- Binding a reference directly to a temporary extends that temporary's lifetime to match the reference, so the object stays usable for the whole block
- The main use of
&&is as a function parameter, since an r-value argument prefers aT&¶meter over aconst T¶meter and lets one function name serve two strategies - A variable of type
T&&is itself an l-value when used in an expression, so it cannot bind to anotherT&¶meter without a cast such asstd::move - Returning
T&&from a function is almost always wrong, because the referenced object is destroyed as the function returns
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Understanding Rvalue References - Quiz
Test your understanding of the lesson.
Practice Exercises
Understanding Rvalue References
Explore the difference between lvalue references and rvalue references. Learn to identify lvalues and rvalues and understand when each type of reference can bind.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!