Consider:
struct A {};
struct B {
operator A&() volatile;
operator A&() const;
operator A&&();
};
B b;
const A& a = b;
(godbolt link: https://godbolt.org/z/jcYch9jdW)
GCC, Clang, and EDG reject this code, complaining that the conversion is ambiguous.
MSVC accepts this code and calls B::operator A&&.
Which compiler is correct, and why?
MSVC is correct according to the current draft standard, but there is an ongoing CWG issue that will instead specify the other compilers' behavior. (Spoiler: it's CWG 1827.)
For const A& a = b;, overload resolution is performed twice.
First, per [dcl.init.ref]/(5.1):
A reference to type “cv1
T1” is initialized by an expression of type “cv2T2” as follows:
If the reference is an lvalue reference and the initializer expression
- [...]
- has a class type (i.e.,
T2is a class type), whereT1is not reference-related toT2, and can be converted to an lvalue of type “cv3T3”, where “cv1T1” is reference-compatible with “cv3T3” (this conversion is selected by enumerating the applicable conversion functions ([over.match.ref]) and choosing the best one through overload resolution),then the reference binds to the initializer expression lvalue in the first case and to the lvalue result of the conversion in the second case (or, in either case, to the appropriate base class subobject of the object).
Here, overload resolution only considers conversion functions that return an lvalue reference.
All compilers agree that b cannot be converted to an lvalue of type const A (or a reference-compatible type) due to the user-defined conversion being ambiguous, but only MSVC proceeds to the next bullets, while other compilers stop with an error here.
The next bullets are
- Otherwise, if the reference is an lvalue reference to a type that is not const-qualified or is volatile-qualified, the program is ill-formed.
const A& is an lvalue reference to a const-qualified type, so we can continue.
Otherwise, if the initializer expression
- [...]
- has a class type (i.e.,
T2is a class type), whereT1is not reference-related toT2, and can be converted to an rvalue of type “cv3T3” or an lvalue of function type “cv3T3”, where “cv1T1” is reference-compatible with “cv3T3” (see [over.match.ref]),then the initializer expression in the first case and the converted expression in the second case is called the converted initializer. [...] In any case, the reference binds to the resulting glvalue (or to an appropriate base class subobject).
For this second overload resolution, b can be converted to an rvalue of type const A using the operator A&& conversion function. So according to the standard, b binds to the result of said conversion function.
Only MSVC implements this to the letter.
But the other compilers' behavior is arguably more intuitive, and as mentioned in CWG 1827, in 2014, the working group agreed that such initialization should be made ill-formed.
CWG agreed that an ambiguity like this should make the initialization ill-formed instead of falling through to do indirect binding.
However, this issue is still in "drafting" status, which indicates that it's not formally a defect report due to a lack of wording.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With