What Does Auto&& Tell Us

What does auto&& tell us?

By using auto&& var = <initializer> you are saying: I will accept any initializer regardless of whether it is an lvalue or rvalue expression and I will preserve its constness. This is typically used for forwarding (usually with T&&). The reason this works is because a "universal reference", auto&& or T&&, will bind to anything.

You might say, well why not just use a const auto& because that will also bind to anything? The problem with using a const reference is that it's const! You won't be able to later bind it to any non-const references or invoke any member functions that are not marked const.

As an example, imagine that you want to get a std::vector, take an iterator to its first element and modify the value pointed to by that iterator in some way:

auto&& vec = some_expression_that_may_be_rvalue_or_lvalue;
auto i = std::begin(vec);
(*i)++;

This code will compile just fine regardless of the initializer expression. The alternatives to auto&& fail in the following ways:

auto         => will copy the vector, but we wanted a reference
auto& => will only bind to modifiable lvalues
const auto& => will bind to anything but make it const, giving us const_iterator
const auto&& => will bind only to rvalues

So for this, auto&& works perfectly! An example of using auto&& like this is in a range-based for loop. See my other question for more details.

If you then use std::forward on your auto&& reference to preserve the fact that it was originally either an lvalue or an rvalue, your code says: Now that I've got your object from either an lvalue or rvalue expression, I want to preserve whichever valueness it originally had so I can use it most efficiently - this might invalidate it. As in:

auto&& var = some_expression_that_may_be_rvalue_or_lvalue;
// var was initialized with either an lvalue or rvalue, but var itself
// is an lvalue because named rvalues are lvalues
use_it_elsewhere(std::forward<decltype(var)>(var));

This allows use_it_elsewhere to rip its guts out for the sake of performance (avoiding copies) when the original initializer was a modifiable rvalue.

What does this mean as to whether we can or when we can steal resources from var? Well since the auto&& will bind to anything, we cannot possibly try to rip out vars guts ourselves - it may very well be an lvalue or even const. We can however std::forward it to other functions that may totally ravage its insides. As soon as we do this, we should consider var to be in an invalid state.

Now let's apply this to the case of auto&& var = foo();, as given in your question, where foo returns a T by value. In this case we know for sure that the type of var will be deduced as T&&. Since we know for certain that it's an rvalue, we don't need std::forward's permission to steal its resources. In this specific case, knowing that foo returns by value, the reader should just read it as: I'm taking an rvalue reference to the temporary returned from foo, so I can happily move from it.


As an addendum, I think it's worth mentioning when an expression like some_expression_that_may_be_rvalue_or_lvalue might turn up, other than a "well your code might change" situation. So here's a contrived example:

std::vector<int> global_vec{1, 2, 3, 4};

template <typename T>
T get_vector()
{
return global_vec;
}

template <typename T>
void foo()
{
auto&& vec = get_vector<T>();
auto i = std::begin(vec);
(*i)++;
std::cout << vec[0] << std::endl;
}

Here, get_vector<T>() is that lovely expression that could be either an lvalue or rvalue depending on the generic type T. We essentially change the return type of get_vector through the template parameter of foo.

When we call foo<std::vector<int>>, get_vector will return global_vec by value, which gives an rvalue expression. Alternatively, when we call foo<std::vector<int>&>, get_vector will return global_vec by reference, resulting in an lvalue expression.

If we do:

foo<std::vector<int>>();
std::cout << global_vec[0] << std::endl;
foo<std::vector<int>&>();
std::cout << global_vec[0] << std::endl;

We get the following output, as expected:

2
1
2
2

If you were to change the auto&& in the code to any of auto, auto&, const auto&, or const auto&& then we won't get the result we want.


An alternative way to change program logic based on whether your auto&& reference is initialised with an lvalue or rvalue expression is to use type traits:

if (std::is_lvalue_reference<decltype(var)>::value) {
// var was initialised with an lvalue expression
} else if (std::is_rvalue_reference<decltype(var)>::value) {
// var was initialised with an rvalue expression
}

What does auto&& do?

The code is right. auto&& p = expr means the type of p is T&& where T will be inferred from expr. The && here indicates a rvalue reference, so e.g.

auto&& p = 1;

will infer T == int and thus the type of p is int&&.

However, references can be collapsed according to the rule:

T& &   == T&
T& && == T&
T&& & == T&
T&& && == T&&

(This feature is used to implement perfect forwarding in C++11.)

In the case

auto&& p = x;

as x is an lvalue, an rvalue reference cannot be bound to it, but if we infer T = int& then the type of p will become int& && = int&, which is an lvalue reference, which can be bound to x. Only in this case auto&& and auto& give the same result. These two are different though, e.g.

auto& p = std::move(x);

is incorrect because std::move(x) is an rvalue, and the lvalue reference cannot be bound to it.

Please read C++ Rvalue References Explained for a walk through.

What's the difference between & and && in a range-based for loop?

7 years after I asked this question, I feel qualified to provide a more complete answer.

I'll start by saying that the code I chose back then is not ideal for the purpose of the question. That's because there is no difference between & and && for the example.

Here's the thing: both

std::vector<int> v = {0, 1, 2, 3, 4, 5};

for (auto& i : v)
{
std::cout << ++i << ' ';
}

std::cout << '\n';

and

std::vector<int> v = {0, 1, 2, 3, 4, 5};

for (auto&& i : v)
{
std::cout << ++i << ' ';
}

std::cout << '\n';

are equivalent.

Here's proof:

#include <vector>

std::vector<int> v;

void f()
{
for (auto& i : v)
{
static_assert(std::is_same<decltype(i), int&>::value);
}

for (auto&& i : v)
{
static_assert(std::is_same<decltype(i), int&>::value);
}
}

But why?

Like David G said in the comments, a rvalue reference to a lvalue reference becomes a lvalue reference due to reference collapsing, eg

#include <type_traits>
using T1 = int&;
using T2 = T1&&;
static_assert(std::is_same<T1, T2>::value);

Note that this, however, is different:

for (int&& i : v)
{
// ...
}

and will fail, since a rvalue reference can't bind to a lvalue. Reference collapsing doesn't apply to this case, since there is no type deduction.

TLDR: for the standard containers, the difference between & and && in a range-based for loop is:

  • value_type& is valid
  • value_type&& is not valid
  • Both auto& and auto&& are equivalent to value_type&

Now let's try the opposite: an iterable object that returns rvalues.

#include <iostream>

struct Generated
{
int operator*() const
{
return i;
}

Generated& operator++()
{
++i;
return *this;
}

bool operator!=(const Generated& x) const
{
return i != x.i;
}

int i;
};

struct Generator
{
Generated begin() const { return { 0 }; }
Generated end() const { return { 6 }; }
};

int main()
{
Generator g;

for (const auto& i : g)
{
std::cout << /*++*/i << ' ';
}
std::cout << '\n';

for (auto&& i : g)
{
std::cout << ++i << ' ';
}
std::cout << '\n';
}

Here, auto& doesn't work, since you can't bind a non-const lvalue to a rvalue.

Now we actually have const int& and int&&:

Generator g;

for (const auto& i : g)
{
static_assert(std::is_same<decltype(i), const int&>::value);
}

for (auto&& i : g)
{
static_assert(std::is_same<decltype(i), int&&>::value);
}

Why does auto&& var2 not imply rvalue reference?

Given that Widget&& var1 is an rvalue reference, why isn't it an rvalue?

someWidget, var1, and var2 all have a name and, therefore, they are all lvalues regardless of their declared type.

The term rvalue reference is about the type of the reference, var1, whereas the term rvalue is about the value category of the expression var1:

  • var1 declared as Widget&& var1 is a reference to Widget, specifically an rvalue reference to Widget (i.e., Widget&&) – This is the type of var1, which is a reference. The reference type tells you how the reference can be initialized: an rvalue reference can only be initialized with an rvalue, whereas a non-const lvalue reference can only be initialized with an lvalue.

  • When using var1 in an expression (without marking it with std::move), then var1 is an lvalue because it has a name – This the value category of var1, which is a property of the expression var1 orthogonal to its type.

Note also that the following statement doesn't compile:

Widget&& var1 = someWidget;

This compilation error is because var1 is an rvalue reference and, therefore, can only be initialized with an rvalue. However, someWidget is an lvalue since it has a name, and it isn't marked with std::move for moving. For the statement to compile you could have done one of the following:

  • Declaring v1 as an lvalue reference instead:

    Widget& var1 = someWidget;

    The lvalue reference var1 can be initialized with someWidget, an lvalue.

  • Marking someWidget for moving with std::move():

    Widget&& var1 = std::move(someWidget);

    The rvalue reference var1 can be initialized with std::move(someWidget), an rvalue.


Why does auto&& var2 not mean rvalue reference?

var2 is a universal reference because there is type deduction involved. As such, var2 will become either an lvalue reference or rvalue reference depending on how it is initialized (var2 will always be a reference).

The auto in auto&& var2 = var1; deduces to Widget& because var1 is an lvalue (i.e., var1 is a named object and you haven't applied std::move() to it). Then, Widget& && results in Widget& due to reference collapsing. To sum up, the statement:

auto&& var2 = var1;

after type deduction becomes:

Widget& var2 = var1;

So var2 is actually an lvalue reference.


If you want var2 to be an rvalue reference, you can apply std::move() to the initializing object:

auto&& var2 = std::move(var1);

after type deduction, this results in:

Widget&& var2 = var1;

var2 is an rvalue reference in this case.

What is the difference between returning auto&& and decltype(auto)?

What is the difference between decltype(auto) and auto&&?

decltype(auto) covers three cases. When returning lvalues, the return type would be T& (lvalue-reference); for xvalues, the return type would be T&& (rvalue-reference); for prvalues, the return type would be T (non-reference, i.e. return by-value).

auto&& covers only two cases. When returning lvalues, the return type would be T& (lvalue-reference); for rvalues, including xvalues and prvalues, the return type would be T&& (rvalue-reference). (Forwarding reference is always a reference.)

What happens if we return rvalue object, like: return int{};? Will return value be dangling reference?

For auto&& the return type is rvalue-reference, so yes, the returned reference is always dangling. For decltype(auto) the return type is non-reference then no such trouble.

When not to use `auto&&`?

  1. Yes. Any return from a function that does not return a reference type may involve a copy/move. Eliding that is what RVO is about. The object that your reference is bound to needs to be initialized somehow.

  2. No. why should it? The lifetime of a temporary/prvalue bound to a reference is determined by the scope of the reference.

  3. If func() does not return a reference type, there should not be any difference in efficiency (nor behaviour)

between

auto&& var = func();

and

auto var = func();

In both cases an object with lifetime to the end of the containing block is constructed. In one case it has its own name, in the other it is named via a reference. In both cases the name can be used as an lvalue. RVO can be equally well applied in either case.

Some compilers might optimize better for a local object than for a reference, even though in the current case the reference-to-temporary is really no different from a local object.

If func() might return a reference, things are much different - in that case you must decide whether you want to copy/move or not.



Related Topics



Leave a reply



Submit