Clang vs GCC vs MSVC template conversion operator - which compiler is right?

后端 未结 1 1173
闹比i
闹比i 2021-01-31 09:44

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

1条回答
  •  再見小時候
    2021-01-31 10:14

    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 type T using a static_cast of the form static_cast(e) if the declaration T t(e); is well-formed, for some invented temporary variable t (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 expression e is used as a glvalue if and only if the initialization uses it as a glvalue.

    Accordingly, the three static_casts 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, 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 T or a type that can be converted to type T via a standard conversion sequence (13.3.3.1.1) are candidate functions. For direct-initialization, those explicit conversion functions that are not hidden within S and yield type T or a type that can be converted to type T 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 cv2 X” return lvalues or xvalues, depending on the type of reference, of type “cv2 X” and are therefore considered to yield X 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 “cv2 T2” 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), where T1 is not reference-related to T2, and can be converted to an lvalue of type “cv3 T3,” where “cv1 T1” is reference-compatible with “cv3 T3” (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 to A. 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 to A 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 and F2 are function template specializations, and the function template for F1 is more specialized than the template for F2 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.

    What if 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 of F2 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 sequence S2 if

    • S1 is a proper subsequence of S2 (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.

    0 讨论(0)
提交回复
热议问题