What is the meaning of a variable with type auto&&?
Asked Answered
C

4

246

If you read code like

auto&& var = foo();

where foo is any function returning by value of type T. Then var is an lvalue of type rvalue reference to T. But what does this imply for var? Does it mean, we are allowed to steal the resources of var? Are there any reasonable situations when you should use auto&& to tell the reader of your code something like you do when you return a unique_ptr<> to tell that you have exclusive ownership? And what about for example T&& when T is of class type?

I just want to understand, if there are any other use cases of auto&& than those in template programming; like the ones discussed in the examples in this article Universal References by Scott Meyers.

Conure answered 5/11, 2012 at 10:46 Comment(6)
I've been wondering the same thing. I understand how the type deduction works, but what is my code saying when I use auto&&? I've been thinking of looking at why a range-based for loop expands to use auto&& as an example, but haven't gotten round to it. Perhaps whoever answers can explain it.Adenine
Is this even legal? I mean instnce of T is destroyed immediatelly after foo returns, storing a rvalue ref to it sounds like UB to ne.Kazim
@aleguna This is perfectly legal. I do not want to return a reference or pointer to a local variable, but a value. The function foo might for example look like: int foo(){return 1;}.Conure
@aleguna references to temporaries perform lifetime extension, just as in C++98.Rhapsodic
@ecatmur, that's something new to me. So if in a function I declare local object on a stack, return a reference to it and someone stores that returned referece, it will work fine? Why do all compilers give warnings when I do this?Kazim
@aleguna lifetime extension only works with local temporaries, not functions returning references. See https://mcmap.net/q/118982/-does-a-const-reference-class-member-prolong-the-life-of-a-temporaryRhapsodic
A
345

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 forwarding 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
}
Adenine answered 5/11, 2012 at 23:37 Comment(8)
Can't we simply say T vec = get_vector<T>(); inside function foo ? Or am I simplifying it to an absurd level :)Jamilla
@Jamilla No bcoz T vec can only be assigned to lvalue in case of std::vector<int&> and if T is std::vector<int> then we will be using call by value which is inefficientPothook
auto& gives me the same result. I'm using MSVC 2015. And GCC produces an error.Balfore
Here I'm using MSVC 2015, auto& gives the same results as that of auto&&.Toyatoyama
Why is int i; auto&& j = i; allowed but int i; int&& j =i; is not ?Amplify
@Amplify An rvalue reference cannot be bound to an lvalue. Therefore, auto&& is deduced to be an int& (lvalue reference) when assigned an lvalue. But int&& is an unconditional rvalue reference. So you'd have to write int&& j = std::move(i); to make your intentions clear and promise at the same time that i will not be used thereafter.Catherincatherina
@SergeyPodobry binding rvalues to non-const lvalue references is a non-standard Microsoft extension.Ops
@Ops Yeah. Newer VS versions work according to the Standard.Balfore
P
15

First, I recommend reading this answer of mine as a side-read for a step-by-step explanation on how template argument deduction for universal references works.

Does it mean, we are allowed to steal the resources of var?

Not necessarily. What if foo() all of a sudden returned a reference, or you changed the call but forgot to update the use of var? Or if you're in generic code and the return type of foo() might change depending on your parameters?

Think of auto&& to be exactly the same as the T&& in template<class T> void f(T&& v);, because it's (nearly) exactly that. What do you do with universal references in functions, when you need to pass them along or use them in any way? You use std::forward<T>(v) to get the original value category back. If it was an lvalue before being passed to your function, it stays an lvalue after being passed through std::forward. If it was an rvalue, it will become an rvalue again (remember, a named rvalue reference is an lvalue).

So, how do you use var correctly in a generic fashion? Use std::forward<decltype(var)>(var). This will work exactly the same as the std::forward<T>(v) in the function template above. If var is a T&&, you'll get an rvalue back, and if it is T&, you'll get an lvalue back.

So, back on topic: What do auto&& v = f(); and std::forward<decltype(v)>(v) in a codebase tell us? They tell us that v will be acquired and passed on in the most efficient way. Remember, though, that after having forwarded such a variable, it's possible that it's moved-from, so it'd be incorrect use it further without resetting it.

Personally, I use auto&& in generic code when I need a modifyable variable. Perfect-forwarding an rvalue is modifying, since the move operation potentially steals its guts. If I just want to be lazy (i.e., not spell the type name even if I know it) and don't need to modify (e.g., when just printing elements of a range), I'll stick to auto const&.


auto is in so far different that auto v = {1,2,3}; will make v an std::initializer_list, whilst f({1,2,3}) will be a deduction failure.

Pose answered 5/11, 2012 at 16:5 Comment(2)
In the first part of your answer: I mean that if foo() returns a value-type T, then var (this expression) will be an lvalue and its type (of this expression) will be a rvalue reference to T (i.e. T&&).Conure
@MWid: Makes sense, removed the first part.Pose
S
4

Consider some type T which has a move constructor, and assume

T t( foo() );

uses that move constructor.

Now, let's use an intermediate reference to capture the return from foo:

auto const &ref = foo();

this rules out use of the move constructor, so the return value will have to be copied instead of moved (even if we use std::move here, we can't actually move through a const ref)

T t(std::move(ref));   // invokes T::T(T const&)

However, if we use

auto &&rvref = foo();
// ...
T t(std::move(rvref)); // invokes T::T(T &&)

the move constructor is still available.


And to address your other questions:

... Are there any reasonable situations when you should use auto&& to tell the reader of your code something ...

The first thing, as Xeo says, is essentially I'm passing X as efficiently as possible, whatever type X is. So, seeing code which uses auto&& internally should communicate that it will use move semantics internally where appropriate.

... like you do when you return a unique_ptr<> to tell that you have exclusive ownership ...

When a function template takes an argument of type T&&, it's saying it may move the object you pass in. Returning unique_ptr explicitly gives ownership to the caller; accepting T&& may remove ownership from the caller (if a move ctor exists, etc.).

Sensualism answered 5/11, 2012 at 11:3 Comment(5)
I'm not sure your second example is valid. Don't you need to you perfect forwarding to invoke the move constructor?Kazim
This is wrong. In both cases the copy constructor is called, since ref and rvref are both lvalues. If you want the move constructor, then you have to write T t(std::move(rvref)).Conure
Did you mean const ref in your first example: auto const &?Snowfall
@aleguna - you and MWid are right, thanks. I've fixed my answer.Sensualism
@Sensualism You are right. But this doesn't answer my quetion. When do you use auto&& and what do you tell the reader of your code by using auto&&?Conure
A
-3

The auto && syntax uses two new features of C++11:

  1. The auto part lets the compiler deduce the type based on the context (the return value in this case). This is without any reference qualifications (allowing you to specify whether you want T, T & or T && for a deduced type T).

  2. The && is the new move semantics. A type supporting move semantics implements a constructor T(T && other) that optimally moves the content in the new type. This allows an object to swap the internal representation instead of performing a deep copy.

This allows you to have something like:

std::vector<std::string> foo();

So:

auto var = foo();

will perform a copy of the returned vector (expensive), but:

auto &&var = foo();

will swap the internal representation of the vector (the vector from foo and the empty vector from var), so will be faster.

This is used in the new for-loop syntax:

for (auto &item : foo())
    std::cout << item << std::endl;

Where the for-loop is holding an auto && to the return value from foo and item is a reference to each value in foo.

Acidulate answered 5/11, 2012 at 11:28 Comment(5)
This is incorrect. auto&& will not move anything, it will just make a reference. Whether it's an lvalue or rvalue reference depends on the expression used to initialize it.Adenine
In both cases will the move constructor be called, since std::vector and std::string are moveconstructible. This has nothing to do with the type of var.Conure
@MWid: Actually, the call to the copy/move constructor could also be elided altogether with RVO.Rost
@MatthieuM. You are right. But I think that in the above example, the copy cnstructor will never get called, since everything is moveconstructible.Conure
@MWid: my point was that even the move constructor can be elided. Elision trumps move (it's cheaper).Rost

© 2022 - 2024 — McMap. All rights reserved.