12

The below fails to compile with clang35 -std=c++11:

#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;
}

with error

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>( )

OTOH, it warns about the narrowing and compiles on g++48 -std=c++11

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

Does either behaviour make sense? Quoting from cppreference

All constructors that take std::initializer_list as the only argument, or as the first argument if the remaining arguments have default values, are examined, and matched by overload resolution against a single argument of type std::initializer_list

If the previous stage does not produce a match, all constructors of T participate in overload resolution against the set of arguments that consists of the elements of the braced-init-list, with the restriction that only non-narrowing conversions are allowed. If this stage produces an explicit constructor as the best match for a copy-list-initialization, compilation fails (note, in simple copy-initialization, explicit constructors are not considered at all)

Since narrowing conversions aren't allowed, I would expect the overload resolution step to not match the A(std::initializer_list<int>) constructor and instead match the A(int, double) one. For example, changing A(std::initializer_list<int>) to A(std::initializer_list<std::string>) compiles with both clang35 and g++48 and prints

A::A(int, double)

as expected.

Barry
  • 286,269
  • 29
  • 621
  • 977
Pradhan
  • 16,391
  • 3
  • 44
  • 59

1 Answers1

11

The behavior makes sense. Scott Meyers has an example almost exactly like this in Effective Modern C++ (emphasis in original):

If, however, one or more constructors declare a parameter of type std::initializer_list, calls using the braced initialization syntax strongly prefer the overloads taking std;:initializer_lists. Strongly. If there's any way for compilers to construe a call using a braced initializer to be a constructor taking a std::initializer_list, compilers will employ that interpretation.

Example using this class:

class Widget {
public:
    Widget(int, bool);
    Widget(int, double);
    Widget(std::initializer_list<long double>);
};

Widget w1(10, true); // calls first ctor
Widget w2{10, true}; // calls std::initializer_list ctor
Widget w3(10, 5.0); // calls second ctor
Widget w4{10, 5.0}; // calls std::initializer_list ctor

Those two calls call the initializer_list ctor even though they involve converting BOTH arguments - and even though the other constructors are perfect matches.

Furthermore:

Compilers' determination to match braced initializers with constructors taking std::initializer_lists is so strong, it prevails even if the best-match std::initializer_list constructor can't be called. For example:

class Widget {
public:
    Widget(int, bool); // as before
    Widget(int, double); // as before
    Widget(std::initializer_list<bool> ); // now bool
};

Widget w{10, 5.0}; // error! requires narrowing conversions

Both compilers pick the correct overload (the initializer_list one) - which we can see is required from the standard (§13.3.1.7):

When objects of non-aggregate class type T are list-initialized (8.5.4), overload resolution selects the constructor in two phases:

(1.1) — 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.
(1.2) — 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.

But calling that particular constructor involves a narrowing. In 8.5.1:

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

So the program is ill-formed. In this case, clang chooses to throw an error while gcc chooses to issue a warning. Both compilers are conforming.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • Which behaviour makes sense? clang and gcc disagree - clang 3.5 fails to compile, while g++ 4.8.3 picks the `initializer_list` overload even in the presence of a narrowing conversion. The example above doesn't illustrate any of the difficulties - the conversions aren't narrowing and hence the `initilalizer_list` overload being picked isn't surprising. – Pradhan Jan 21 '15 at 02:09
  • 5
    We might add that this _strong preference_ is awesome because then you can e.g. `std::vector { "hello", "world" }` because the compiler _makes the effort_ to try and see the initializer list as initializers for objects of type `std::string`. Otherwise we would be stuck forever writing `std::vector { std::string{"hello"}, std::string{"world"} }`. _And imagine the pain when you tried to initiailize something [less trivial](http://stackoverflow.com/a/28036210)_. – sehe Jan 21 '15 at 02:12
  • @Pradhan adding narrowing example from the book – Barry Jan 21 '15 at 02:17
  • 1
    @Barry Both compilers are conforming. Narrowing makes the program *ill-formed*, which only means that the compiler must issue a diagnostic; GCC's warning satisfies this requirement. – T.C. Jan 21 '15 at 02:17
  • @T.C. I see lots of examples that explicitly say `// error: narrowing` – Barry Jan 21 '15 at 02:20
  • @Barry Examples are not normative, and in any event the standard does not distinguish between errors and warnings. Both are diagnostic messages. – T.C. Jan 21 '15 at 02:23
  • @Barry Thanks for the answer. I think the emphasis should be on the last part of your answer since the rest of it is summarized(not in as much detail of course) in the question. The crucial point seems to be that overload resolution picks the `initializer_list` constructor if there is a conversion from the elements of the `braced-init-list` to `X`. However, this is *ill-formed* if any of these conversions are narrowing. – Pradhan Jan 21 '15 at 04:18
  • @T.C.: A "diagnostic" is whatever the implementation says is a diagnostic (but without the proper documentation, neither an error nor warning message technically qualifies). A compiler can also require specific flags to conform, such as one to treat some or all warnings as errors. – Jerry Coffin Jan 21 '15 at 07:42
  • Since gcc 5.1 its an error now. The interesting thing is that gcc will not issue this warning for `X x5{0,10.5};` or `X x5{1,10.5};`, it looks like it finds 0 and 1 , and since it can be easily converted to false and true, it does not look further. – marcinj Jan 17 '17 at 00:44