c++language-lawyeroverload-resolutionconversion-operator

Overload resolution between conversion operators to value and to const-reference in C++


In the following program struct B defines two conversion operators: to A and to const A&. Then A-object is created from B-object:

struct A {};

struct B {
  A a;
  B() = default;
  operator const A&() { return a; }
  operator A() { return a; }
};

int main() {
  (void) A(B{});
}

The program is

GCC error message is

error: call of overloaded 'A(B)' is ambiguous
note: candidate: 'constexpr A::A(const A&)'
note: candidate: 'constexpr A::A(A&&)'

Which compiler is right here?


Solution

  • The implementation divergence is probably related to CWG 2327.

    If look strictly at the wording of C++20, then GCC is right and the overload resolution is ambiguous. I'll go into the wording in detail first, and then at the end of the answer I'll discuss CWG 2327 again.

    There are two candidates for the initialization:

    A::A(const A&);
    A::A(A&&);
    

    The first step is to determine the implicit conversion sequence required to call each candidate: the ICS from "rvalue of B" to const A&, and the ICS from "rvalue of B" to A&&. The value category of the B is not actually relevant, though, because neither conversion function in B has a ref-qualifier.

    To convert from B to const A& or to A&&, we go to [dcl.init.ref]. For the conversion to const A&, p5.1.2 applies:

    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

      • [...]
      • 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 (12.4.2.7) and choosing the best one through overload resolution (12.4)),

      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).

    This applies because B can be converted to an lvalue of type const A (this is the T3), and const A (as T1) is reference-compatible with const A (as T3).

    To convert from B to A&&, the applicable rule is p5.3.2, which is very similar except that this time we are only looking for conversion functions that yield an rvalue of some type T3.

    12.4.2.7, a.k.a. [over.match.ref] explains how to find the candidate conversion functions:

    [...] 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 (9.4.4) with “cv2 T2”, are candidate functions. [...]

    When initializing const A&, obviously operator const A&() is one of the candidates. operator A() is not a candidate since it doesn't yield an lvalue. (The fact that a const A& can be initialized from an A rvalue is irrelevant; as you can see from the wording above, there is no special casing for const references in [over.match.ref].) When initializing A&&, operator A() is a candidate, and operator const A&() is not, because it doesn't yield an rvalue.

    Thus, we have the following implicit conversion sequences:

    The basic rule for ranking user-defined conversion sequences, [over.ics.rank]/3.3, is that if two ICSes use the same user-defined conversion function, the one whose second standard conversion sequence is better is considered to be the overall better ICS. This rule doesn't apply here because the two conversion functions are different. The tie-breaker rules in p4 of this section do not prefer one over the other. So finally we have to go to the global tie-breaker rules in [over.match.best.general]. p2.2 seems like it might be relevant:

    • Given these definitions, a viable function F1 is defined to be a better function than another viable function F2 if for all arguments i, ICSi(F1) is not a worse conversion sequence than ICSi(F2), and then
    • the context is an initialization by user-defined conversion (see 9.4, 12.4.2.6, and 12.4.2.7) 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 or, if not that, [...]

    However, a proper understanding of the wording reveals that these rules don't pick out either constructor as being better than the other, either. Although our overload resolution involved a "subtask" overload resolution to select user-defined conversion functions, those subtasks have already been completed and we are no longer in the "context" of an initialization by user-defined conversion; we are in the context of selecting the best constructor.

    So there is no rule that can be used to select either constructor over the other; the overload resolution fails.

    There does not seem to be any wording from earlier standard editions that would allow this to compile.

    But if you look at the discussion of CWG 2327 on the linked page, you'll see that Richard Smith suggests that a change be made to the initialization rules. Under the current rules, since A is a class type, the overload resolution always involves enumerating constructors of A and picking the best candidate, which we discussed above, which might involve as "subtasks" the consideration of conversion functions from B to types required by A's constructors. Smith has informally proposed that the conversion functions of B be considered at the top level alongside the constructors of A. However, there is currently no proposed wording explaining how to rank such conversion functions against constructors.

    If there are three possible candidates for the initialization, namely

    then it would be reasonable for the third option to be considered better than the other two, and I suspect that Smith has already come up with some rule and implemented it in Clang, but I'm not sure what it is. I'm sure he'll add wording to the issue once he's worked out all the cases for the wording. If this is indeed the case, then it makes sense that Clang accepts the code (by calling operator A instead of a constructor) only in C++17 mode and later, where guarantee copy elision applies. As for MSVC, perhaps they have a proposed resolution that they've decided to apply all the way back to C++14 (and probably C++11 as well).