Search code examples
c++for-loopreferencervalue-reference

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


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.


Solution

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