Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Understanding Rvalue References
Understand r-value references and when to use them instead of pointers.
R-value references
In an earlier chapter, we discussed value categories, which are properties of expressions that help determine whether an expression resolves to a value, function, or object. We introduced l-values and r-values when discussing l-value references.
If value categories seem unclear, now is the perfect time to review them, as they'll be central to this chapter.
L-value references recap
Prior to C++11, only one reference type existed in C++. In C++11, it's called an l-value reference. L-value references can only be initialized with modifiable l-values.
| L-value reference | Can be initialized with | Can modify |
|---|---|---|
| Modifiable l-values | Yes | Yes |
| Non-modifiable l-values | No | No |
| R-values | No | No |
L-value references to const objects can be initialized with modifiable l-values, non-modifiable l-values, and r-values. However, these values cannot be modified.
| L-value reference to const | Can be initialized with | Can modify |
|---|---|---|
| Modifiable l-values | Yes | No |
| Non-modifiable l-values | Yes | No |
| R-values | Yes | No |
L-value references to const are especially useful for passing any argument type (l-value or r-value) into functions without copying.
R-value references
C++11 introduces a new reference type called an r-value reference. An r-value reference is designed to be initialized with an r-value only. While an l-value reference uses a single ampersand, an r-value reference uses a double ampersand:
int count{10};
int& lref{count}; // l-value reference initialized with l-value count
int&& rref{10}; // r-value reference initialized with r-value 10
R-value references cannot be initialized with l-values.
| R-value reference | Can be initialized with | Can modify |
|---|---|---|
| Modifiable l-values | No | No |
| Non-modifiable l-values | No | No |
| R-values | Yes | Yes |
| R-value reference to const | Can be initialized with | Can modify |
|---|---|---|
| Modifiable l-values | No | No |
| Non-modifiable l-values | No | No |
| R-values | Yes | No |
R-value references have two useful properties. First, they extend the lifetime of the object they're initialized with to match the r-value reference's lifetime (l-value references to const can also do this). Second, non-const r-value references allow modifying the r-value!
Let's examine some examples:
#include <iostream>
class Temperature
{
private:
int m_celsius{0};
public:
Temperature(int celsius = 0)
: m_celsius{celsius}
{
}
friend std::ostream& operator<<(std::ostream& out, const Temperature& temp)
{
out << temp.m_celsius << " degrees C";
return out;
}
};
int main()
{
auto&& rref{Temperature{25}}; // r-value reference to temporary Temperature
// temp parameter binds to the temporary, no copies created
std::cout << rref << '\n';
return 0;
} // rref (and the temporary Temperature) goes out of scope here
This program prints:
25 degrees C
As an anonymous object, Temperature{25} would normally be destroyed at the end of its expression. However, since we initialize an r-value reference with it, its lifetime extends until block end. We can then use that r-value reference to print the Temperature's value.
Now a less intuitive example:
#include <iostream>
int main()
{
int&& rref{5}; // a temporary with value 5 is created here
rref = 10;
std::cout << rref << '\n';
return 0;
}
This program prints:
10
While it seems odd to initialize an r-value reference with a literal and then modify it, when initializing an r-value reference with a literal, a temporary object is constructed from the literal. The reference references this temporary object, not the literal value directly.
R-value references are rarely used in these ways.
R-value references as function parameters
R-value references are most commonly used as function parameters. This is particularly useful for function overloads when different behavior is needed for l-value and r-value arguments.
#include <iostream>
void process(const int& lref) // l-value arguments select this function
{
std::cout << "l-value reference to const: " << lref << '\n';
}
void process(int&& rref) // r-value arguments select this function
{
std::cout << "r-value reference: " << rref << '\n';
}
int main()
{
int value{10};
process(value); // l-value argument calls l-value version
process(10); // r-value argument calls r-value version
return 0;
}
This prints:
l-value reference to const: 10
r-value reference: 10
When passed an l-value, the overloaded function resolves to the l-value reference version. When passed an r-value, it resolves to the r-value reference version (considered a better match than an l-value reference to const).
Why is this useful? We'll discuss this in detail in the next lesson. It's a crucial part of move semantics.
Rvalue reference variables are lvalues
Consider this code:
int&& ref{5};
process(ref);
Which version of process does this call: process(const int&) or process(int&&)?
The answer might surprise you. This calls process(const int&).
Although variable ref has type int&&, when used in an expression it is an lvalue (like all named variables). An object's type and its value category are independent.
You already know that literal 5 is an rvalue of type int, and int x is an lvalue of type int. Similarly, int&& ref is an lvalue of type int&&.
Not only does process(ref) call process(const int&), it doesn't even match process(int&&) because rvalue references cannot bind to lvalues.
Returning an r-value reference
You should almost never return an r-value reference, for the same reason you should almost never return an l-value reference. In most cases, you'll return a hanging reference when the referenced object goes out of scope at function end.
Summary
L-value references recap: L-value references (single ampersand) can be initialized with modifiable l-values and allow modification. L-value references to const can be initialized with l-values and r-values but prohibit modification.
R-value references: R-value references (double ampersand) are a C++11 feature designed to be initialized only with r-values. They extend the lifetime of the temporary they reference and allow modification of that temporary.
R-value reference properties: Non-const r-value references allow modifying r-values. When initialized with a temporary, the r-value reference extends that temporary's lifetime to match the reference's lifetime.
Function overloading: R-value references are most useful as function parameters to distinguish between l-value and r-value arguments. This enables different behavior for temporary objects versus persistent objects.
Value category vs type: An object's type and its value category are independent. An r-value reference variable (e.g., int&& ref) has type int&& but is itself an l-value when used in expressions, because all named variables are l-values.
R-value reference variables are l-values. Even though int&& ref has type int&&, when used in an expression, ref is an l-value. This means it cannot bind to r-value reference parameters.
Almost never return an r-value reference from a function. This typically creates a hanging reference when the referenced object goes out of scope at function end.
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!