I have simple code with conversion operator and it seems like all compilers are giving different results, was curious which compiler, if any, is correct? I tried different combi
In short: Clang is correct (though in one case, for the wrong reason). GCC is wrong in the second case. MSVC is wrong in the first case.
Let's start from static_cast
(§5.2.9 [expr.static.cast]/p4, all quotes are from N3936):
An expression
e
can be explicitly converted to a typeT
using astatic_cast
of the formstatic_cast
if the declaration(e) T t(e);
is well-formed, for some invented temporary variablet
(8.5). The effect of such an explicit conversion is the same as performing the declaration and initialization and then using the temporary variable as the result of the conversion. The expressione
is used as a glvalue if and only if the initialization uses it as a glvalue.
Accordingly, the three static_cast
s here are effectively three initializations:
int t1(call_operator{});
const int & t2(call_operator{});
int & t3(call_operator{});
Note that we rewrote call_operator()
as call_operator{}
for exposition purposes only, as int t1(call_operator());
is the most vexing parse. There is a small semantic difference between those two forms of initialization, but that difference is immaterial to this discussion.
int t1(call_operator{});
The applicable rule for this initialization is set out in §8.5 [dcl.init]/p17:
if the source type is a (possibly cv-qualified) class type, conversion functions are considered. The applicable conversion functions are enumerated (13.3.1.5), and the best one is chosen through overload resolution (13.3). The user-defined conversion so selected is called to convert the initializer expression into the object being initialized. If the conversion cannot be done or is ambiguous, the initialization is ill-formed.
We proceed to §13.3.1.5 [over.match.conv], which says:
Assuming that “
cv1 T
” is the type of the object being initialized, and “cv S
” is the type of the initializer expression, withS
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 withinS
and yield typeT
or a type that can be converted to typeT
via a standard conversion sequence (13.3.3.1.1) are candidate functions. For direct-initialization, those explicit conversion functions that are not hidden withinS
and yield typeT
or a type that can be converted to typeT
with a qualification conversion (4.4) are also candidate functions. Conversion functions that return a cv-qualified type are considered to yield the cv-unqualified version of that type for this process of selecting candidate functions. Conversion functions that return “reference to cv2X
” return lvalues or xvalues, depending on the type of reference, of type “cv2X
” and are therefore considered to yieldX
for this process of selecting candidate functions.2 The argument list has one argument, which is the initializer expression. [ Note: This argument will be compared against the implicit object parameter of the conversion functions. —end note ]
The candidate set, after template argument deduction, is:
operator T() - with T = int
operator const T& () const - with T = int
operator T&() const - with T = int
The argument list consists of the single expression call_operator{}
, which is non-const. It therefore converts better to the non-const implicit object parameter of operator T()
than to the other two. Accordingly, operator T()
is the best match and is selected by overload resolution.
const int & t2(call_operator{});
This initialization is governed by §8.5.3 [dcl.init.ref]/p5:
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
- is an lvalue (but is not a bit-field), and “cv1 T1” is reference-compatible with “cv2
T2
,” or- has a class type (i.e.,
T2
is a class type), whereT1
is 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 (13.3.1.6) and choosing the best one through overload resolution (13.3)).then the reference is bound 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).
Note that this step considers only conversion functions returning lvalue references.
Clang appears to deduce the candidate set as*:
operator const T& () const - with T = int
operator T&() const - with T = int
It's obvious that the two functions are tied on the implicit object parameter since both are const
. Further, since both are direct reference bindings, by §13.3.3.1.4 [ics.ref]/p1, the conversion required from the return type of either function to const int &
is the identity conversion. (Not Qualification Adjustment - that refers to the conversion described in §4.4 [conv.qual], and is only applicable to pointers.)
However, it appears that the deduction performed by Clang for operator T&()
in this case is incorrect‡. §14.8.2.3 [temp.deduct.conv]/p5-6:
5 In general, the deduction process attempts to find template argument values that will make the deduced
A
identical toA
. However, there are two cases that allow a difference:
- If the original
A
is a reference type,A
can be more cv-qualified than the deduced A (i.e., the type referred to by the reference)- The deduced
A
can be another pointer or pointer to member type that can be converted toA
via a qualification conversion.6 These alternatives are considered only if type deduction would otherwise fail. If they yield more than one possible deduced
A
, the type deduction fails.
Since type deduction can succeed by deducing T
as const int
for operator T&()
for an exact match between the deduced type and the destination type, the alternatives shouldn't be considered, T
should have been deduced as const int
, and the candidate set is actually
operator const T& () const - with T = int
operator T&() const - with T = const int
Once again, both standard conversion sequences from the result are identity conversions. GCC (and EDG, thanks to @Jonathan Wakely for testing) correctly deduces T
in operator T&()
to be const int
in this case*.
Regardless of the correctness of the deduction, however, the tiebreaker here is the same. Because, according to the partial ordering rules for function templates, operator const T& ()
is more specialized than operator T&()
(due to the special rule in §14.8.2.4 [temp.deduct.partial]/p9), the former wins by the tiebreaker in §13.3.3 [over.match.best]/p1, 2nd list, last bullet point:
F1
andF2
are function template specializations, and the function template forF1
is more specialized than the template forF2
according to the partial ordering rules described in 14.5.6.2.
Thus, in this case, Clang gets the right result, but for (partially) the wrong reason. GCC gets the right result, for the right reason.
int & t3(call_operator{});
There is no fight here. operator const T&();
simply can't possibly be used to initialize a int &
. There is only one viable function, operator T&()
with T = int
, so it is the best viable function.
operator const T&();
isn't const
?The only interesting case here is the initialization int t1(call_operator{});
. The two strong contenders are:
operator T() - with T = int
operator const T& () - with T = int
Note that the rule regarding ranking standard conversion sequences - §13.3.3 [over.match.best]/p1, 2nd list, 2nd bullet point:
the context is an initialization by user-defined conversion (see 8.5, 13.3.1.5, and 13.3.1.6) and the standard conversion sequence from the return type of
F1
to the destination type (i.e., the type of the entity being initialized) is a better conversion sequence than the standard conversion sequence from the return type ofF2
to the destination type.
and §13.3.3.2 [over.ics.rank]/p2:
Standard conversion sequence
S1
is a better conversion sequence than standard conversion sequenceS2
if
S1
is a proper subsequence ofS2
(comparing the conversion sequences in the canonical form defined by 13.3.3.1.1, excluding any Lvalue Transformation; the identity conversion sequence is considered to be a subsequence of any non-identity conversion sequence)
cannot distinguish these two, because the conversion necessary to get an int
from a const int &
is an lvalue-to-rvalue conversion, which is an Lvalue Transformation. After excluding the Lvalue Transformation, the standard conversion sequences from the result to the destination type are identical; nor does any of the other rules in §13.3.3.2 [over.ics.rank] apply.
Thus the only rule that could possibly distinguish between these two functions is again the "more specialized" rule. The question is then whether one of operator T()
and operator const T&()
is more specialized than the other. The answer is no. The detailed partial ordering rules are rather complex, but an analogous situation is easily found in the example in §14.5.6.2 [temp.func.order]/p2, which labels a call to g(x)
as ambiguous given:
template
void g(T); template void g(T&);
A quick perusal of the procedure specified in §14.8.2.4 [temp.deduct.partial] confirms that given one template taking a const T&
and the other taking a T
by value, neither is more specialized than the other**. Thus, in this case, there is no unique best viable function, the conversion is ambiguous, and the code is ill-formed.†
* The type deduced by Clang and GCC for the operator T&()
case is determined by running the code with operator const T&()
removed.
** Briefly, during the deduction for partial ordering, before any comparison is done, reference types are replaced with the types referred to, and then top-level cv-qualifiers are stripped, so both const T&
and T
yield the same signature. However, §14.8.2.4 [temp.deduct.partial]/p9 contains a special rule for when both types at issue were reference types, which makes operator const T&()
more specialized than operator T&()
; that rule doesn't apply when one of the types is not a reference type.
† GCC appears to not consider operator const T&()
a viable conversion for this case, but does consider operator T&()
a viable conversion.
‡ This appears to be Clang bug 20783.