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

1

28

I'm wondering what's the difference between for (auto& i : v) and for (auto&& i : v) in a range-based for loop like in this code:

#include <iostream>
#include <vector>

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

    std::cout << "Initial values: ";

    for (auto i : v)    // Prints the initial values
        std::cout << i << ' ';
    std::cout << '\n';

    for (auto i : v)    // Doesn't modify v because i is a copy of each value
        std::cout << ++i << ' ';
    std::cout << '\n';

    for (auto& i : v)   // Modifies v because i is a reference
        std::cout << ++i << ' ';
    std::cout << '\n';

    for (auto&& i : v)  // Modifies v because i is a rvalue reference (Am I right?)
        std::cout << ++i << ' ';
    std::cout << '\n';

    for (const auto &i : v) // Wouldn't compile without the /**/ because i is const
        std::cout << /*++*/i << ' ';
    std::cout << '\n';

}

The output:

Initial values: 0 1 2 3 4 5
1 2 3 4 5 6
1 2 3 4 5 6
2 3 4 5 6 7
2 3 4 5 6 7

Both seem to do the same thing here but I'd like to know what's the difference between for (auto& i : v) and for (auto&& i : v) in this code.

Klug answered 28/3, 2015 at 17:54 Comment(10)
auto uses template argument deduction rules. So auto by itself deduces the type by value, auto& deduces by reference, and auto&& is not an rvalue reference but rather uses reference collapsing rules to deduce the type.Podagra
@0x499602D2 Thanks for your answer. I got the "deduces the type by value" part but what does deduce by reference means? Is there something about that I can read that can help me understand it?Klug
Simply put, auto& is an lvalue-reference so it requires an lvalue for its initializer. The elements of the vector can be iterated over as lvalues, so it works. It's also useful when you want to prevent copying each element. This video should explain everything else - vimeo.com/97344493Podagra
@0x499602D2 Okay. This is what I think I got: using only auto would copy the object to i (already knew this one). And there's no difference between & and && in this case because there aren't rvalues, only lvalues. Is this right?Klug
Yes, that's correct. But if the elements being iterated are rvalues, auto by itself will allow a move. And it can only bind to auto&& because rvalues cannot bind to lvalue-references.Podagra
@0x499602D2 Thanks. Also, how could possibly the elements being iterated be rvalues? Maybe a std::initializer_list?Klug
@0x499602D2 PS: Also, you could (and should) post this as an answer.Klug
@Klug std::vector<bool> is the most notorious example.Isma
You need a special kind of iterator like what std::vector<bool> has. It can't just be done automatically, even if you do for (auto& x : {1, 2, 3}) the elements are still iterated over as lvalues.Podagra
Ah. So the mysterious and unique phrase on en.cppreference.com/w/cpp/language/range-for, "deduction to forwarding reference," just means reference collapsing rules.Brickwork
K
9

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); 
}
Klug answered 13/5, 2022 at 21:49 Comment(3)
(This answer seems great but still trying to understand..) Just for future reference: 1) See [§8.6.4 The range-based for statement] of C++20 standard to understand how range-based for loop works internally. 2) See @DavidG's comment mentioning 'deduction'. Another example relating deduction(from comments in another answer): int i; auto&& j=i; is compiled because auto&& is deduced to be an int& (lvalue reference) when assigned an lvalue. (auto&& does not become int&& because an rvalue reference cannot be bound to an lvalue).Impersonality
"Both auto& and auto&& are equivalent to value_type&" ->Does this also applies to structured binding in a range-based for loop? I.e., for (auto& [k,v] : mymap){...} and for (auto&& [k,v] : mymap){...} are the same?Impersonality
@starriet I don't see why it wouldn't: godbolt.org/z/ebG1Wq61rKlug

© 2022 - 2024 — McMap. All rights reserved.