10

I tested the following code on Visual Studio and it compiles and prints "A(double)".

#include <iostream>
#include <initializer_list>

struct A {
    A(std::initializer_list<int>) { puts("initializer_list<int>"); }        // (1)
    A(std::initializer_list<float>) { puts("initializer_list<float>"); }    // (2)
    A(double) { puts("A(double)"); }                                        // (3)
};

int main() {
    A var{ 1.1 };   
}

However both IntelliSense and http://ideone.com/ZS1Mcm disagree, saying that more than one instance of constructor "A::A" matches the argument list (meaning both initializer-list constructors). Note that if either (1) or (2) is removed, code does not compile anymore, as "conversion from 'double' to 'float' requires a narrowing conversion".

Is this a bug? The behaviour feels inconsistent, but I see the same behaviour in VS13 and VS15 so maybe there is more to it?

Barry
  • 286,269
  • 29
  • 621
  • 977
Karlis Olte
  • 353
  • 1
  • 3
  • 10
  • Related: [Why doesn't narrowing affect overload resolution?](http://stackoverflow.com/q/31730222/) – dyp Jul 31 '15 at 20:33

2 Answers2

3

The code is ill-formed. §8.5.4/(3.6) applies:

Otherwise, if T is a class type, constructors are considered. The applicable constructors are enumerated and the best one is chosen through overload resolution (13.3, 13.3.1.7).

Now, §13.3.3.1.5 goes

When an argument is an initializer list (8.5.4), it is not an expression and special rules apply for converting it to a parameter type. […] 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.

Converting 1.1, which is of type double (!), to int is a Floating-integral conversion with Conversion rank, while the conversion from 1.1 to float is a Floating point conversion - also having Conversion rank.

enter image description here

Thus both conversions are equally good, and since §13.3.3.2/(3.1) cannot distinguish them either, the call is ambiguous. Note that narrowing doesn't play a role until after overload resolution is done and hence cannot affect the candidate set or the selection process. More precisely, a candidate must meet the requirement set in 13.3.2/3:

Second, for F to be a viable function, there shall exist for each argument an implicit conversion sequence (13.3.3.1) that converts that argument to the corresponding parameter of F.

However, as shown in the second quote, the implicit conversion sequence that converts {1.1} to std::initializer_list<int> is the worst conversion from 1.1 to int, which is a Floating-integral conversion - and a valid (and existing!) one at that.


If instead you pass {1.1f} or alter the initializer_list<float> to <double>, the code is well-formed, as converting 1.1f to float is an identity conversion. The standard gives a corresponding example in (3.6):

[ Example:

struct S {
    S(std::initializer_list<double>); // #1
    S(std::initializer_list<int>);    // #2

};
S s1 = { 1.0, 2.0, 3.0 }; // invoke #1

end example ]

Even more interestingly,

struct S {
    S(std::initializer_list<double>); // #1
    S(std::initializer_list<int>);    // #2

};
S s1 = { 1.f }; // invoke #1 

Is also valid - because the conversion from 1.f to double is a Floating point promotion, having Promotion rank, which is better than Conversion rank.

Columbo
  • 60,038
  • 8
  • 155
  • 203
  • It seems the list-init sequence is not an ICS [over.best.ics]p3. I'm quite confused. Do we apply [over.ics.list]p4 to discover that there is an ICS? – dyp Jul 30 '15 at 20:20
  • Well it is quite clear that we'll have to look into [over.ics.list] to proceed :) However, I don't quite see the step from [over.match.viable] to [over.ics.list]. The latter almost seems tacked on as an afterthought. – dyp Jul 30 '15 at 20:56
  • @dyp Sorry for that nonsensical comment. Yeah, I suppose that the connection isn't as clear as it could be; A DR would be appropriate (and I could find a corresponding one, yet). – Columbo Sep 08 '15 at 18:08
2

Visual Studio is wrong in accepting your code - it is ill-formed, and should fail to compile. gcc and clang are correct in rejecting it.

The key bullet point in list-initialization here is:

Otherwise, if T is a class type, constructors are considered. The applicable constructors are enumerated and the best one is chosen through overload resolution (13.3, 13.3.1.7). If a narrowing conversion (see below) is required to convert any of the arguments, the program is ill-formed.

For which we reference [over.match.list]:

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.

We have two such candidate functions - (1) and (2). Neither is a better match than the other, since both conversions from double to int and float have Conversion rank. So the call is ambiguous. Note that even though there is a candidate with Exact Rank (A(double )), we consider the initializer_list constructors first.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • But (1) is not a viable function, because of the required narrowing conversion. Why is this a hard error? – dyp Jul 30 '15 at 16:19
  • @dyp It is treated as a viable function though (possible defect?) In [over.ics.list], there is "Otherwise, if the parameter type is `std::initializer_list` 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." No mention of narrowing, and `double` *can* be implicitly converted to `int`. – Barry Jul 30 '15 at 16:23
  • Wow. I was tricked by the wording: "When an argument is an initializer list", but I read "When an argument is an `initializer_list`". However, I think the parameter type is *not* `initializer_list<..>`, it is a reference, so we need to consider [over.ics.ref] instead of [over.ics.list] (maybe we'll get to [over.ics.list] in a second step?) – dyp Jul 30 '15 at 16:29
  • Also, I'm not convinced (yet) that [over.ics.list] defines the well-formedness of a conversion. It seems to specify the ICS for ranking. [over.match.list] defines *candidates*, and as far as I can tell, [over.match.viable] should remove the non-viable `initializer_list` candidate. – dyp Jul 30 '15 at 16:38
  • 1
    @dyp I think narrowing conversions are still viable conversions. I don't see anything to suggest that `struct A { A(int ); A(float ); }; A{1.1};` shouldn't be ambiguous. – Barry Jul 30 '15 at 16:48
  • @dyp Concerning the narrowing bit, check out the latter part of my answer. – Columbo Jul 30 '15 at 17:06
  • You answered my original question, but not now my original question seems kinda silly. In my mind I was focusing only on one context and it did not come to my mind to actually consider initializer_list itself. – Karlis Olte Jul 30 '15 at 17:13
  • @KarlisOlte I edited your question to clarify what I consider your intent to be. If you disagree, feel free to rollback. – Barry Jul 30 '15 at 17:17
  • @Barry *"I think narrowing conversions are still viable conversions"* It certainly seems the compiler implementers agree with that interpretation. The relation between *implicit conversion sequence* and *implicit conversion* (as defined in [conv]p3) is still not clear to me though, for example the "governed by the rules for initialization of an object or reference by a single expression" in [over.best.ics]p1. p3 doesn't even mention the list-init sequence as an ICS. – dyp Jul 30 '15 at 20:17
  • @dyp Isn't an *implicit conversion sequence* just the sequence of standard/user-defined conversions that make an *implicit conversion* viable? Anyway, I think Columbo's answer is the better one here. – Barry Jul 30 '15 at 20:23
  • AFAIK, initializing an `initializer_list` from a braced-init-list is neither a standard conversion (not in [conv]) nor a user-defined conversion (neither a constructor nor a conversion function is called). It is weird compiler magic. I can only make sense of it by skipping ahead from [over.match.viable]p3 to [over.ics.list]p4, then discover there is an ICS, and back to [over.best.ics] to continue. – dyp Jul 30 '15 at 20:27