c++initializer-listperfect-forwardingrange-based-loop

Range for loop for empty initializer list


I was reading about forwarding references on cpp reference https://en.cppreference.com/w/cpp/language/reference#Forwarding_references and I was interested to learn that there is a special case for forwarding references:

auto&& z = {1, 2, 3}; // *not* a forwarding reference (special case for initializer lists)

So I started experimenting on godbolt (as a side note I'd be interested to know why this special case is needed). I was slightly surprised to find I could iterate over an initialiser list like so:

for (auto&& x : {1, 2, 3})
{
    // do something 
}

Until I realised that x was deduced as int and that therefore the following wouldn't work:

for (auto&& x : {{1}})
{
    // do something
}

So I think here, Auto couldn't deduce the initialiser list, because of the special case mentioned above?

Then I tried an empty list, which didn't compile also:

for (auto&& x : {})
{
    // do something
}

The compiler error message using GCC suggests that this is because it couldn't deduce auto from the empty list, so I then tried the following:

for (int x : {})
{
    // do something
}

To explicitly tell the compiler that it's an empty list of type int. This surprised me, I expected that since I had explicitly given the type, it could deduce what {} was, especially since iterating over a populated version of the initialiser list worked. After some experimentation I found that the following line also does not compile:

auto x{};

So I think the reason you can't iterate over the empty initialiser list is because it cannot deduce the inner type and therefore can't construct it in the first place.

I would like some clarity on my thoughts and reasoning here


Solution

  • Let's start with the special case:

    auto&& z = {1, 2, 3};
    

    In this case, auto&& isn't really deducible to anything, because initializer lists must always receive their type from the context where they're used (such as function parameters, copy initialization, etc.

    However, the language has added a few "fallback cases" where we simply treat such initializer lists as std::initializer_list. The example above is one of those cases, and z will be of type std::initializer_list<int>&&. I.e., z is not a forwarding reference, but an rvalue reference. We know that it's std::initializer_list<int> because all of the expressions in {1, 2, 3} are int.

    Note: the term "initializer list" refers to the language construct {...} (as in list initialization), which is not necessarily std::initializer_list.

    Initializer lists in for-loops

    for (auto&& x : {1, 2, 3}) { /* ... */ }
    

    We can make sense of what happens by expanding it:

    /* init-statement */
    auto &&__range = {1, 2, 3};
    auto __begin = begin(__range)
    auto __end = end(__range);
    
    for ( ; __begin != __end; ++__begin) {
        auto&& x = *begin;
        /* ... */
    }
    

    This is exactly what range-based for loops expand to since C++20.

    Note that x is a forwarding reference here, and that has nothing to do with std::initializer_list. It always is, regardless of what we're iterating over.

    Anyhow, : {1, 2, 3} works because we initialize __range with it, just like in the original example with z. __range will then be an rvalue reference to a std::initializer_list.

    Broken cases

    {{1}}
    

    doesn't compile because the inner {1} cannot deduce what is being initialized using the braces here. This is not one of those cases where we can fall back onto std::initializer_list.

    for (auto x : {})
    // and
    for (int x : {})
    

    These two also don't work, because as you've seen in the expansion above, the int isn't giving any hint as to what type the std::initializer_list should be. The type of the loop variable is being used in an entirely different place, so we end up with auto &&__range = {} in both cases, which is not allowed.

    auto x = {};
    // or
    auto x{};
    

    Are broken in the same way. With an empty initializer list, we have no way of knowing what type the std::initializer_list should be.