Juergen
Juergen

Reputation: 3743

Possible bug in clang or gcc because of deleted templated lvalue conversion operator?

This is a follow-up of this question. I played with this code and used clang trunk and gcc trunk:

struct A
{   
};

struct T
{
    A a;
    operator A() { return a; }
    
    template <typename T> operator const T&() = delete;
};

struct C
{   
    A a;
};

int main()
{
    C c;
    T t;

    c.a = t;
}

Demo

Clang has nothing to complain, but gcc has:

<source>: In function 'int main()':
<source>:23:11: error: use of deleted function 'T::operator const T&() [with T = A]'
   23 |     c.a = t;
      |           ^
<source>:10:27: note: declared here
   10 |     template <typename T> operator const T&() = delete;
      |  

                     ^~~~~~~~

So, which compiler is right and which is wrong?

I would like to see gcc is wrong, but how can I overcome the error?

Upvotes: 4

Views: 155

Answers (1)

Both Clang and GCC are wrong, but at least GCC stops compilation so it's marginally better.

A is a class type, and as such assigning to it passes through an operator= overload. You did not provide one, so the compiler provides two to the effect of

A& operator=(A const&) = default;
A& operator=(A&&) = default;

That reference argument is what needs to get initialized from t in the expression c.a = t. And for either operator=, the reference can be bound unambiguously.

[dcl.init.ref]

5 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 ([over.match.ref]) and choosing the best one through overload resolution),

    then the reference is bound ... to the lvalue result of the conversion in the second case (or, in either case, to the appropriate base class subobject of the object).

  • Otherwise, if 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 rvalue or function lvalue of type “cv3 T3”, where “cv1 T1” is reference-compatible with “cv3 T3” (see [over.match.ref]),

    then ... the result of the conversion in the second case is called the converted initializer. If the converted initializer is a prvalue, its type T4 is adjusted to type “cv1 T4” ([conv.qual]) and the temporary materialization conversion ([conv.rval]) is applied. In any case, the reference is bound to the resulting glvalue (or to an appropriate base class subobject).

Where on the subject of building that candidate set for these two cases, the standard says

[over.match.ref]

1 Under the conditions specified in [dcl.init.ref], a reference can be bound directly to 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 “reference to cv1 T” is the 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 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” (when initializing an lvalue reference or an rvalue reference to function) or “rvalue reference to cv2 T2” (when initializing an rvalue reference or an lvalue reference to function), where T2 is the same type as T or can be converted to type T with a qualification conversion, are also candidate functions.

That bullet needs a bit of work to parse correctly, but it basically describes one of two disjoint cases that may apply here:

  1. Initialization of an lvalue reference to T
    • The candidate functions are those that yield "lvalue reference to cv2 T2".
  2. Initialization of an rvalue reference to T
    • The candidate functions are those that yield "cv2 T2" or "rvalue reference to cv2 T2".

For operator=(A const&) we are in case #1, and have a synthesized operator A const&() as the only candidate. For operator=(A&&) its case #2, and the non-template operator A() is the only candidate. Either way, we have an unambiguous implicit conversion sequence with a user-defined conversion that binds the reference parameter of either operator=.

But now neither operator= is a better viable function than the other according to the rules in [over.match.best]. And neither conversion is better according to the partial ordering in [over.ics.rank].

This means that the the program should be declared ill-formed on account of an ambiguous call to operator=. However, GCC and Clang both err (not MSVC though)1. Clang favors the A&& overload, while GCC goes for A const& and issues a diagnostic for the use of a deleted conversion function. But this isn't due to any standard mandated behavior. Ideally, they should both report the call to operator= is ambiguous.


1 - A comparison of different compiler behaviors, with a reduced example.

Upvotes: 5

Related Questions