6

Consider the following code snippet:

#include <iostream>
#include <initializer_list>

struct C
{
    C(std::initializer_list<int>) { std::cout << "list\n"; }
    C(std::initializer_list<int>, std::initializer_list<int>) { std::cout << "twice-list\n"; }
};

int main()
{
    C c1 { {1,2}, {3} }; // twice-list ctor
    C c2 { {1}, {2} }; // why not twice-list ?
    return 0;
}

Live demo.

Why scalar values in braces for c2 variable are not interpreted as separate std::initializer_list?

αλεχολυτ
  • 4,792
  • 1
  • 35
  • 71

2 Answers2

4

First, something very important: You have two different kinds of constructors. The first in particular, C(std::initializer_list<int>), is called an initializer-list constructor. The second is just a normal user-defined constructor.

[dcl.init.list]/p2

A constructor is an initializer-list constructor if its first parameter is of type std::initializer_list<E> or reference to possibly cv-qualified std::initializer_list<E> for some type E, and either there are no other parameters or else all other parameters have default arguments (8.3.6).

In a list-initialization containing one or more initializer-clauses, initializer-list constructors are considered before any other constructors. That is, initializer-list constructors are initially the only candidates during overload resolution.

[over.match.list]/p1

When objects of non-aggregate class type T are list-initialized such that 8.5.4 specifies that overload resolution is performed according to the rules in this section, overload resolution selects the constructor in two phases:

  • Initially, the candidate functions are the initializer-list constructors (8.5.4) of the class T and the argument list consists of the initializer list as a single argument.

  • If no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all the constructors of the class T and the argument list consists of the elements of the initializer list.

So for both declarations of c1 and c2 the candidate set consists only of the C(std::initializer_list<int>) constructor.

After the constructor is selected the arguments are evaluated to see if there exists an implicit conversion sequence to convert them to the parameter types. This takes us to the rules for initializer-list conversions:

[over.ics.list]/p4 (emphasis mine):

Otherwise, if the parameter type is std::initializer_list<X> and all the elements of the initializer list can be implicitly converted to X, the implicit conversion sequence is the worst conversion necessary to convert an element of the list to X, or if the initializer list has no elements, the identity conversion.

This means a conversion exists if each element of the initializer list can be converted to int.

Let's focus on c1 for now: For the initializer-list {{1, 2}, {3}}, the initializer-clause {3} can be converted to int ([over.ics.list]/p9.1), but not {1, 2} (i.e int i = {1,2} is ill-formed). This means the condition for the above quote is violated. Since there is no conversion, overload resolution fails since there are no other viable constructors and we are taken back to the second phase of [over.match.list]/p1:

  • If no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all the constructors of the class T and the argument list consists of the elements of the initializer list.

Notice the change in wording at the end. The argument list in the second phase is no longer a single initializer-list, but the arguments of the braced-init-list used in the declaration. This means we can evaluate the implicit conversions in terms of the initializer-lists individually instead of at the same time.

In the initializer-list {1, 2}, both initializer-clauses can be converted to int, so the entire initializer-clause can be converted to initializer_list<int>, the same for {3}. Overload resolution is then resolved with the second constructor being chosen.

Now let's focus on c2, which should be easy now. The initializer-list constructor is first evaluated, and, using { {1}, {2} } there surely exists a conversion to int from {1} and {2}, so the first constructor is chosen.

David G
  • 94,763
  • 41
  • 167
  • 253
  • So, why if I'll change second constructor to `C(std::initializer_list, std::initializer_list)` and `c2` declaration to `C c2 { {1.0}, {2} };` I'll get compiler error? According to your answer must be selected second constructor for this. Or I was made wrong conclusion? [live demo](http://coliru.stacked-crooked.com/a/e299e66462e220cf) – αλεχολυτ Jul 04 '15 at 08:23
  • 1
    @alexolut [over.isc.list]/p4: "[...]and all the elements of the initializer list can be implicitly converted to `X`," A `double` can be implicitly converted to `int`, so this clause holds. Reading further: "the implicit conversion sequence is the worst conversion necessary to convert an element of the list to `X`". This implicit conversion sequence is the *narrowing conversion sequence*. So it's not that a viable initializer list constructor wasn't found (going to phase 2 means we haven't found one), it's that the conversion itself causes the program to be ill-formed. – David G Jul 04 '15 at 13:00
  • I.e. viable constructor was found, but argument conversion itself not viable (ill-formed). In this case we are not reached second phase. - am I understand it correct? – αλεχολυτ Jul 04 '15 at 13:24
  • 1
    @alexolut Yes, that's correct. Also note: "If a narrowing conversion [..] is required to convert any of the arguments, the program is ill-formed." – David G Jul 04 '15 at 13:28
  • @alexolut [I just found a great answer that goes more in-depth about why narrowing doesn't affect overload resolution.](http://stackoverflow.com/questions/31730222/why-doesnt-narrowing-affect-overload-resolution) – David G Jul 31 '15 at 16:08
0

C c2 { {1}, {2} };

This line does not pass in two arguments of std::initializer_list<int>, but rather, it is passing in one std::initializer_list<std::initializer_list<int> >. A solution would be to instead instantiate c2 like so:

C c2({1}, {2});

ross
  • 526
  • 5
  • 19
  • 1
    The question is "why". – Cheers and hth. - Alf Jul 03 '15 at 16:38
  • I see. Let me grab a reference really quick and edit. – ross Jul 03 '15 at 16:38
  • Basically, the answer to "why" is because std::initializer_list has a special priority when it appears in a constructor. If you have a constructor with a std::initializer_list and you construct the object with brace initialization, the compiler will do his best to convert what you inputted to fit the std::initializer_list constructor, even if it's not the best fit – KABoissonneault Jul 03 '15 at 16:47