2
#include <iostream>
#include <string>
#include <initializer_list>

class A
{
 public:
  A(int, bool) { std::cout << __PRETTY_FUNCTION__ << std::endl; }
  A(int, double) { std::cout << __PRETTY_FUNCTION__ << std::endl; }
  A(std::initializer_list<int>) { std::cout << __PRETTY_FUNCTION__ << std::endl; }
};

int main()
{
  A a1 = {1, 1.0};
  return 0;
}

(This question is a follow-up to this.)

The above program fails to compile with clang35 -std=c++11

init.cpp:15:14: error: type 'double' cannot be narrowed to 'int' in initializer list [-Wc++11-narrowing]
  A a1 = {1, 1.0};
             ^~~
init.cpp:15:14: note: insert an explicit cast to silence this issue
  A a1 = {1, 1.0};
             ^~~
             static_cast<int>( )

while g++48 -std=c++11 chooses a produce a warning to diagnose the ill-formed narrowing

init.cpp: In function ‘int main()’:
init.cpp:15:17: warning: narrowing conversion of ‘1.0e+0’ from ‘double’ to ‘int’ inside { } [-Wnarrowing]
   A a1 = {1, 1.0};
                 ^
init.cpp:15:17: warning: narrowing conversion of ‘1.0e+0’ from ‘double’ to ‘int’ inside { } [-Wnarrowing]

and produces the result

A::A(std::initializer_list<int>)

My question is if A::A(std::initializer_list<int>) should be a viable overload. Below are standard quotes that I think imply that the initializer_list overload should not be viable.

From 13.3.2 [over.match.viable]

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

From 4 [conv]

An expression e can be implicitly converted to a type T if and only if the declaration T t=e; is well-formed, for some invented temporary variable t.

From 8.5.1 [dcl.init.aggr]

If the initializer-clause is an expression and a narrowing conversion is required to convert the expression, the program is ill-formed.

Using 8.5.1 and 4, since the following is not well-formed

std::initializer_list<int> e = {1, 1.0};

{1, 1.0} is not implicitly convertible to std::initializer_list<int>.

Using the quote from 13.3.2, shouldn't it imply that A::A(std::initializer_list<int>) isn't a viable function when doing overload resolution for A a1 = {1, 1.0};? Finding no viable initializer_list constructors, shouldn't this statement pick A::A(int, double)?

Community
  • 1
  • 1
Pradhan
  • 16,391
  • 3
  • 44
  • 59
  • 1
    How does 8.5.1 apply for `int t = 1.0;`? That's not aggregate initialization, is it? – Columbo Jan 21 '15 at 18:29
  • 1
    It was stated in the post that you linked to that constructors that take an initializer list are strongly favored. So the compiler picks the initializer list constructor and then tries to convert and since it narrows the compilation fails. – NathanOliver Jan 21 '15 at 18:30
  • @Columbo Sorry, I didn't get your point. Also, for `8.5.1`, I didn't paste the entire standard quote, but since you mention aggregate initialization, I guess that is clear enough. – Pradhan Jan 21 '15 at 18:33
  • 1
    @NathanOliver Yes, but "strong preference" is Scott Meyers putting the standard in simple English. I am asking how it follows from the standardese. – Pradhan Jan 21 '15 at 18:33
  • @Pradhan I posted an answer to make my point more clear. – Columbo Jan 21 '15 at 18:35

1 Answers1

4

I believe that the problem in your analysis is the fact that the statement

int t = 1.0;

is indeed well-formed - an implicit conversion from double to int obviously exists. [over.ics.list]/4 also describes it:

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.

Every element from the initializer list can be implicitly converted to int, thus the constructor is viable and chosen. However, only once it is chosen, the whole thing hard-errors, [dcl.init.list]/(3.6):

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.

As you can see, the constructor to call is determined before the narrowing-check is performed. In other words, the viability of an initializer-list constructor is not depending on narrowing of any arguments.
Thus the code should be ill-formed.

One way to get the desired behavior is to use a constructor template with SFINAE

template <typename T, typename=std::enable_if_t<std::is_same<int, T>{}>>
A(std::initializer_list<T>) { std::cout << __PRETTY_FUNCTION__ << std::endl; }

Demo.

Columbo
  • 60,038
  • 8
  • 155
  • 203
  • Ah ok, that seems to explain it. Going over those parts to ensure I understand. What do you think is the standard specifies it in this seemingly convoluted way? Why not simply ensure that in it isn't a viable overload? Is it to avoid complicating aggregate initialization? IOW they would have to differentiate between ` = {1, 1.0}` and ` = {1, 1.0}`? – Pradhan Jan 21 '15 at 18:58
  • 1
    @Pradhan I have no idea tbh. It seems that either we're missing some corner-cases or the section is defected. – Columbo Jan 21 '15 at 19:01
  • I think it would be strange if the constructor used was dependend on the value of the argument (for conversions from integer to integer type, where the source is a constant expression, for example). – dyp Jan 21 '15 at 20:25