C++17: explicit conversion function vs explicit constructor + implicit conversions – have the rules changed?

  • A+
Category:Languages

Clang 6, clang 7, and gcc 7.1, 7.2, and 7.3 all agree that the following is valid C++17 code, but is ambiguous under C++14 and C++11. MSVC 2015 and 2017 accept it as well. However, gcc-8.1 and 8.2 reject it even in c++17 mode:

struct Foo {     explicit Foo(int ptr); };  template<class T> struct Bar {     operator T() const;     template<typename T2>     explicit operator T2() const; };   Foo foo(Bar<char> x) {     return (Foo)x; } 

The compilers that accept it pick the templated explicit conversion function, Bar::operator T2().

The compilers that reject it agree that there is an ambiguity between:

  1. the explicit conversion function Bar::operator int()
  2. first using the implicit user-defined conversion from Bar<char> to char, then the implicit built-in conversion from char to int, and then the explicit constructor Foo(int).

So, which compiler is right? What is the relevant difference in the standard between C++14 and C++17?


Appendix: actual error messages

Here's the error for gcc-8.2 -std=c++17. gcc-7.2 -std=c++14 prints the same error:

<source>: In function 'Foo foo(Bar<char>)':     <source>:17:17: error: call of overloaded 'Foo(Bar<char>&)' is ambiguous          return (Foo)x;                      ^     <source>:3:14: note: candidate: 'Foo::Foo(int)'          explicit Foo(int ptr);                   ^~~     <source>:1:8: note: candidate: 'constexpr Foo::Foo(const Foo&)'      struct Foo             ^~~     <source>:1:8: note: candidate: 'constexpr Foo::Foo(Foo&&)' 

And here's the error from clang-7 -std=c++14 (clang-7 -std=c++17 accepts the code):

<source>:17:12: error: ambiguous conversion for C-style cast from 'Bar<char>' to 'Foo'         return (Foo)x;                ^~~~~~     <source>:1:8: note: candidate constructor (the implicit move constructor)     struct Foo            ^     <source>:1:8: note: candidate constructor (the implicit copy constructor)     <source>:3:14: note: candidate constructor         explicit Foo(int ptr);                  ^     1 error generated. 

 


There are several forces at play here. To understand what's happening, let's examine where (Foo)x should lead us. First and foremost, that c-style cast is equivalent to a static_cast in this particular case. And the semantics of the static cast would be to direct-initialize the result object. Since the result object would be of a class type, [dcl.init]/17.6.2 tells us it's initialized as follows:

Otherwise, if the initialization is direct-initialization, or if it is copy-initialization where the cv-unqualified version of the source type is the same class as, or a derived class of, the class of the destination, constructors are considered. The applicable constructors are enumerated ([over.match.ctor]), and the best one is chosen through overload resolution. The constructor so selected is called to initialize the object, with the initializer expression or expression-list as its argument(s). If no constructor applies, or the overload resolution is ambiguous, the initialization is ill-formed.

So overload resolution to pick the constructor of Foo to call. And if overload resolution fails, the program is ill-formed. In this case, it shouldn't fail, even though we have 3 candidate constructors. Those are Foo(int), Foo(Foo const&) and Foo(Foo&&).

For the first ,we need to copy initialize an int as an argument to the constructor, and that means find an implicit conversion sequence from Bar<char> to int. Since the user defined conversion operator you provided from Bar<char> to char is not explicit, we can use it to from an implicit conversation sequence Bar<char> -> char -> int.

For the other two constructors, we need to bind a reference to a Foo. However, we cannot do that. According to [over.match.ref]/1 :

Under the conditions specified in [dcl.init.ref], a reference can be bound directly to a glvalue or class prvalue that is the result of applying a conversion function to an initializer expression. Overload resolution is used to select the conversion function to be invoked. Assuming that “cv1 T” is the underlying type of the reference being initialized, and “cv S” is the type of the initializer expression, with S a class type, the candidate functions are selected as follows:

  • The conversion functions of S and its base classes are considered. Those non-explicit conversion functions that are not hidden within S and yield type “lvalue reference to cv2 T2” (when initializing an lvalue reference or an rvalue reference to function) or “ cv2 T2” or “rvalue reference to cv2 T2” (when initializing an rvalue reference or an lvalue reference to function), where “cv1 T” is reference-compatible ([dcl.init.ref]) with “cv2 T2”, are candidate functions. For direct-initialization, those explicit conversion functions that are not hidden within S and yield type “lvalue reference to cv2 T2” or “cv2 T2” or “rvalue reference to cv2 T2,” respectively, where T2 is the same type as T or can be converted to type T with a qualification conversion ([conv.qual]), are also candidate functions.

The only conversion function that can yield us a glvalue or prvalue of type Foo is a specialization of the explicit conversion function template you specified. But, because initialization of function arguments is not direct initialization, we cannot consider the explicit conversion function. So we cannot call the copy or move constructors in overload resolution. That leaves us only with the constructor taking an int. So overload resolution is a success, and that should be it.

Then why do some compilers find it ambiguous, or call the templated conversion operator instead? Well, since guaranteed copy elision was introduced into the standard, it was noted (CWG issue 2327) that user defined conversion functions should also contribute to copy elision. Today, according to the dry letter of the standard, they do not. But we'd really like them to. While the wording for exactly how it should be done is still being worked out, it would seem that some compilers already go ahead and try to implement it.

And it's that implementation that you see. It's the opposing force of extending copy elision that interferes with overload resolution here.

Comment

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: