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.

Warning
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.
Danger
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.