What Does 'Auto && E' Do in Range-Based For-Loops

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);
}

C++11 Range-based for-loop efficiency const auto &i versus auto i

Yes. The same reason if you only ever read an argument you make the parameter const&.

T        // I'm copying this
T& // I'm modifying this
const T& // I'm reading this

Those are your "defaults". When T is a fundamental type (built-in), though, you generally just revert to const T (no reference) for reading, because a copy is cheaper than aliasing.


I have a program that I'm developing in which I'm considering making this change throughout, since efficiency is critical in it

  1. Don't make blind sweeping changes. A working program is better than a fast but broken program.
  2. How you iterate through your loops probably won't make much of a difference; you're looping for a reason, aren't you? The body of your loop will much more likely be the culprit.
  3. If efficiency is critical, you want to use a profiler to find which parts of your program are actually slow, rather than guess at parts that might be slow. See #2 for why your guess may be wrong.

What is the advantage of using forwarding references in range-based for loops?

The only advantage I can see is when the sequence iterator returns a proxy reference and you need to operate on that reference in a non-const way. For example consider:

#include <vector>

int main()
{
std::vector<bool> v(10);
for (auto& e : v)
e = true;
}

This doesn't compile because rvalue vector<bool>::reference returned from the iterator won't bind to a non-const lvalue reference. But this will work:

#include <vector>

int main()
{
std::vector<bool> v(10);
for (auto&& e : v)
e = true;
}

All that being said, I wouldn't code this way unless you knew you needed to satisfy such a use case. I.e. I wouldn't do this gratuitously because it does cause people to wonder what you're up to. And if I did do it, it wouldn't hurt to include a comment as to why:

#include <vector>

int main()
{
std::vector<bool> v(10);
// using auto&& so that I can handle the rvalue reference
// returned for the vector<bool> case
for (auto&& e : v)
e = true;
}

Edit

This last case of mine should really be a template to make sense. If you know the loop is always handling a proxy reference, then auto would work as well as auto&&. But when the loop was sometimes handling non-proxy references and sometimes proxy-references, then I think auto&& would become the solution of choice.

What is the difference between regular for statement and range-based for statement in C++

The difference in your case is, that the first version with iterators, well, uses iterators (that's why cout << i << endl; is not working), and the second version (the range-based for loop) gives you either a copy, a reference, or const reference.

So this:

for(auto i = vec.begin(); i != vec.end(); i++)
{
cout << i << endl; // should be *i
}

uses iterators (vec.begin() gives you an iterator to the first element).

Whereas this:

for(auto i : vec)
{
cout << i << endl;
}

uses copies of elements in your vector.

While this:

for(auto& i : vec)
{
cout << i << endl;
}

uses references to your vector elements.

Range-based for loop on a temporary range

Note that using a temporary as the range expression directly is fine, its lefetime will be extended. But for f()[5], what f() returns is the temporary and it's constructed within the expression, and it'll be destroyed after the whole expression where it's constructed.

From C++20, you can use init-statement for range-based for loop to solve such problems.

(emphasis mine)

If range_expression returns a temporary, its lifetime is extended
until the end of the loop, as indicated by binding to the rvalue
reference __range, but beware that the lifetime of any temporary
within range_expression is not extended
.

This problem may be worked around using init-statement:

for (auto& x : foo().items()) { /* .. */ } // undefined behavior if foo() returns by value
for (T thing = foo(); auto& x : thing.items()) { /* ... */ } // OK

e.g.

for(auto thing = f(); auto e : thing[5])
std::cout << e << std::endl;


Related Topics



Leave a reply



Submit