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. Howev
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
to int
. Since the user defined conversion operator you provided from Bar
to char
is not explicit, we can use it to from an implicit conversation sequence Bar
.
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.