Search code examples
c++type-conversionoverloadingoverload-resolution

Overload resolution of user-defined type conversion


I considered a type conversion from one type to the other, which are defined by two way, i.e., type conversion constructor and type conversion function.

struct to_type;
struct from_type{
   operator to_type()const;
};
struct to_type{
   to_type() = default;
   to_type(const from_type&){}
};
from_type::operator to_type()const{return to_type();}

int main(){
    from_type From;
    to_type To;
    To = From;

    return 0;
}

gcc (v13.0.0, but seems to be same even in v4.9.4) don't throw any error and just call type conversion constructor in the above code.

On the other hand, clang (v16.0.0, but seems to be same even in v7.6.0) throw a "ambiguous" compile error like the following.

prog.cc:14:10: error: reference initialization of type 'to_type &&' with initializer of type 'from_type' is ambiguous
    To = From;
         ^~~~
prog.cc:3:4: note: candidate function
   operator to_type()const;
   ^
prog.cc:7:4: note: candidate constructor
   to_type(const from_type&){}
   ^
prog.cc:5:8: note: passing argument to parameter here
struct to_type{
       ^
1 error generated.

It seems to be so curious that two major compiler show different result for this simple code. Is either compiler don't match with the standard for C++? I guess this overload resolution related to [over.ics.rank], but I could not concluded which compiler's behavior match to the standard.

Or do my source code contains undefined behavior?


[ADD 2022-12-21T00:20Z]

Following the comment by Artyer, I tried -pedantic compile option for gcc, and now gcc also output error message.

prog.cc: In function 'int main()':
prog.cc:14:10: error: conversion from 'from_type' to 'to_type' is ambiguous
   14 |     To = From;
      |          ^~~~
prog.cc:9:1: note: candidate: 'from_type::operator to_type() const'
    9 | from_type::operator to_type()const{return to_type();}
      | ^~~~~~~~~
prog.cc:7:4: note: candidate: 'to_type::to_type(const from_type&)'
    7 |    to_type(const from_type&){}
      |    ^~~~~~~
prog.cc:5:8: note:   initializing argument 1 of 'constexpr to_type& to_type::operator=(to_type&&)'
    5 | struct to_type{
      |        ^~~~~~~

This suggests that at least the default behavior of gcc without -pedantic don't match with the requirements of C++ standard for this source code.


Solution

  • In this case clang is right, this behaviour is defined in [over.best.ics.general], and standard even mentions that such a conversion is ambiguous explicitly under the sample code to [over.best.ics.general]/10 (the scenario under the link is in fact considers another kind of ambiguity, but resolution to user-defined pair of conversion constructor and conversion operator is one of the candidates, so I removed the part of the code with another candidate):

    class B;
    class A { A (B&);};
    class B { operator A (); };
    ...
    void f(A) { }
    ...
    B b;
    f(b); // ... an (ambiguous) conversion b → A (via constructor or conversion function)
    

    In order to break the name resolution down, I'd like to represent the conversion sequence (To = From;) as an assignment operator function:

    to_type& operator=(to_type&& param) { } // defined implicitly by the compiler
    

    The actual conversion happens when param needs to get into existence out of From argument of type from_type. The compiler then needs to decide step by step:

    1. Which type the conversion sequence from from_type to to_type&& is of? (standard/user defined/ellipsis):

      • The types from_type is user defined, but to_type&& is of reference type, and reference binding could be considered identity (i.e. standard) conversion. However it's not the case, since from_type and to_type are not the same and cannot be bound directly ([over.ics.ref]/2):

      When a parameter of reference type is not bound directly to an argument expression, the conversion sequence is the one required to convert the argument expression to the referenced type according to [over.best.ics].

      • Without reference binding there is no any other standard conversion sequence that may suit here. Let's consider user-defined conversion. [over.ics.user] gives us the following definition:

      A user-defined conversion sequence consists of an initial standard conversion sequence followed by a user-defined conversion ([class.conv]) followed by a second standard conversion sequence. If the user-defined conversion is specified by a constructor ([class.conv.ctor]), the initial standard conversion sequence converts the source type to the type of the first parameter of that constructor. If the user-defined conversion is specified by a conversion function, the initial standard conversion sequence converts the source type to the type of the implicit object parameter of that conversion function.

      This sounds about right to me: we need to convert from_type argument to to_type temporary in order for to_type&& to bind to the argument, thus the sequence is either

      • from_type -> const from_type& for the converting constructor argument to_type(const from_type&) -> to_type&& for the move-assignment operator of to_type& operator=(to_type&&)

      OR

      • from_type -> implicit-object-parameter of type const from_type& for conversion function operator to_type() const -> to_type&& for the move-assignment operator of to_type& operator=(to_type&&).
    2. Now we have two possible conversion sequences of the same kind (user-defined). For this scenario [over.best.ics.general]/10 says the following:

    If there are multiple well-formed implicit conversion sequences converting the argument to the parameter type, the implicit conversion sequence associated with the parameter is defined to be the unique conversion sequence designated the ambiguous conversion sequence. For the purpose of ranking implicit conversion sequences as described in [over.ics.rank].

    The Ranking implicit conversion sequences documentation then gives the following clues about deciding on which conversion (of the same sequence type) should take precedence for user-defined sequences ([over.ics.rank]/3.3, emphasis mine):

    User-defined conversion sequence U1 is a better conversion sequence than another user-defined conversion sequence U2 if they contain the same user-defined conversion function or constructor or they initialize the same class in an aggregate initialization and in either case the second standard conversion sequence of U1 is better than the second standard conversion sequence of U2

    Here we go, for both scenarios (with the converting constructor and the conversion function) the second standard conversion is of the same type (a temporary of type to_type to to_type&&), thus the operations are indistinguishable.