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:
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, andzwill be of typestd::initializer_list<int>&&. I.e.,zis not a forwarding reference, but an rvalue reference. We know that it'sstd::initializer_list<int>because all of the expressions in{1, 2, 3}areint.Note: the term "initializer list" refers to the language construct
{...}(as in list initialization), which is not necessarilystd::initializer_list.Initializer lists in for-loops
We can make sense of what happens by expanding it:
This is exactly what range-based for loops expand to since C++20.
Note that
xis a forwarding reference here, and that has nothing to do withstd::initializer_list. It always is, regardless of what we're iterating over.Anyhow,
: {1, 2, 3}works because we initialize__rangewith it, just like in the original example withz.__rangewill then be an rvalue reference to astd::initializer_list.Broken cases
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 ontostd::initializer_list.These two also don't work, because as you've seen in the expansion above, the
intisn't giving any hint as to what type thestd::initializer_listshould be. The type of the loop variable is being used in an entirely different place, so we end up withauto &&__range = {}in both cases, which is not allowed.Are broken in the same way. With an empty initializer list, we have no way of knowing what type the
std::initializer_listshould be.