Search code examples
c++c++11language-lawyeroverload-resolutionlist-initialization

Why is this initializer_list constructor a viable overload?


#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)?


Solution

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