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
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
.
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
.
{{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.