List-initialization and failed overload resolution of initializer_list constructor
Asked Answered
H

1

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.

Heaton answered 21/1, 2015 at 1:58 Comment(2)
Presumably you mean Clang 3.5. What you named the binary isn't really helpful :) mv clang25 clang35 "oops"Coady
You are right :) This is the versioning convention used by the team maintaining build systems at work and I never gave it a second thought.Heaton
S
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.

Spall answered 21/1, 2015 at 2:5 Comment(9)
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.Heaton
We might add that this strong preference is awesome because then you can e.g. std::vector<std::string> { "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> { std::string{"hello"}, std::string{"world"} }. And imagine the pain when you tried to initiailize something less trivial.Notorious
@Heaton adding narrowing example from the bookSpall
@Spall 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.Maudemaudie
@Maudemaudie I see lots of examples that explicitly say // error: narrowingSpall
@Spall Examples are not normative, and in any event the standard does not distinguish between errors and warnings. Both are diagnostic messages.Maudemaudie
@Spall 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<X> 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.Heaton
@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.Adonic
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.Winnow

© 2022 - 2024 — McMap. All rights reserved.