Range for loop for empty initializer list

223 Views Asked by At

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

2

There are 2 best solutions below

0
Jan Schultke On BEST ANSWER

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.

1
foragerDev On

For the case, you are specifying the type of x not for the initializers_list. It will compile if element in initializer_list can be converted x for example I can use char instead of int it will compile if my types supports this conversion. So int is the type of the variable x. Compiler is not getting any clue of the type of initializer_list.

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

To explicitly tell about your type you have to tell its type like this:

for(auto i: std::initializer_list<int>{})
{

}

For example this code will print abc:

for(char i: {97, 98, 99}) 
{
    std::cout << i;
}