Jonathan Boccara's blog

What auto&& means

Published April 2, 2021 - 0 Comments

Since C++11, we have a && in the language, and it can take some time to understand its meaning and all the consequences this can have on your code.

We’ve been through a detailed explanation of lvalues, rvalues and their references, which covers a lot of ground on this topic.

But there is one aspect that we have to talk about: what does auto&&, X&&, or even int&& means in code:

auto&& x = f(); // what is happening here?
int&& i = 42; // what does this mean? does it even mean anything?

If anything, this can help us better understand how the references of modern C++ work.

auto&&, a lambda template parameter

C++14 introduced a position in the language where auto (or auto&, auto const& or auto&&) can occur: in lambdas.

Those lambdas are then the equivalent of template member functions in function objects.

For instance, consider this code:

std::for_each(begin(numbers), end(numbers), [](auto&& value){ value += 1; });

Notice the auto&& in the lambda. Its function object equivalent would be this:

struct MyFunction
{
    template<typename T>
    void operator()(T&& value)
    {
        value += 1;
    }
};

// ...

std::for_each(begin(numbers), end(numbers), MyFunction{});

This auto&& in lambda can be useful to implement generic code. For example, the code of the pipes library uses this a lot to make its implementation more expressive.

If you’re familiar with forwarding references, all this should be pretty clear. (If you’re not familiar with forwarding references, check out the last section of this refresher).

auto&&, a variable

But there is another property of auto&&, when it is applied on variables, and not on template parameters. Contrary to template lambdas, which appeared in C++14, this usage of auto&& is available since C++11.

Consider the following code:

X getX();

// ...

auto&& rx = getX();

What does this mean?

As Scott Meyers explains it in Effective Modern C++ (in the item 2), the rules for type deduction of auto are the same as those of templates (apart from one exception: curly braces in auto are interpreted as std::initializer_lists).

This means that in the above line of code, rx is a forwarding reference, so an lvalue reference if initialized from an lvalue, and an rvalue reference if initialized from an rvalue.

In our case, getX() is an rvalue, so rx is an rvalue reference.

But what good is it, since it refers to a temporary object, that is supposed to be destroyed after the end of the statement? Going even further, is this code dangerous, as rx would become a dangling reference after the end of the statement?

Lifetime extension

It turns out that the above code is not dangerous, because the temporary object is not destroyed after the end of the statement where it is instantiated. The rvalue reference extends its lifetime, until the reference itself is destroyed, when it gets out of scope.

This is very similar to what Herb Sutter calls the most important const: when a const reference binds to a temporary object, the lifetime of this object is extended to the point where the reference is destroyed.

&& has the same effect as const& here: it extends the life of the temporary.

To me, this feature has not been as widely communicated as the most important const.

Let’s check that the rvalue reference keeps the temporary alive with a simple test: let’s add a log in the destructor of the temporary:

struct X
{
    ~X(){ std::cout << "destruct X\n"; }
};

We have a function to create the temporary:

X getX()
{
    return {};
}

Now let’s add some logging to follow along what is happening during the execution:

int main()
{
    std::cout << "before getX()\n";
    auto&& rx = getX();
    std::cout << "after getX()\n";
}

When we execute this program, we get the following output (run it yourself here):

before getX()
after getX()
destruct X

We can see that the temporary object was not destroyed on the statement it was created, but at the end of the scope of rx. The rvalue reference extended its lifetime.

When can that be useful? A case I see is when the returned object is not moveable (for example an std::array), in a case where the RVO does not apply, and when we’d like to modify this value (so we wouldn’t use const&).

What int&& means

Now just for fun, let’s think about what the following line of code means:

int&& ri = 42;

First, does this compile? The answer is yes.

ri is a rvalue reference, because int&& designates an rvalue reference (since it is not a template nor an auto, it is not a forwarding reference).

Which makes us wonder, what is 42?

This is a deep philosophical question, but fortunately one that has an answer: 42 is the Answer to the Ultimate Question of Life, the Universe, and Everything.

But from a C++ point of view, what is 42? Its type is int. But what about its value category?

To find out, we can force the compiler to tell us, by creating a compile error where the message describes 42. Consider the following code:

int& ri = 42;

It fails to compile, with the following error message:

error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'

The compiler says that 42 is “an rvalue of type ‘int'”. So in our code that compiles:

int&& ri = 42;

ri is an rvalue reference to 42, which expression is an rvalue.

This is now clear, but this was for fun, because we can just as well take a copy of 42:

int ri = 42;

Indeed, there shouldn’t be a performance advantage in creating a reference over copying an int, and int&& i = 42 is way, way more mysterious that the good old int i = 42. So no point really.

But if anything, this experiment can make us better understand the types, the categories, and the references of C++. And it was fun.

You will also like

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin