Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is my explicit constructor creating this ambiguity for my conversion operator?

I'm unable to figure out why my conversion operator is considering the explicit constructor.

#include <utility>

template <typename T = void>
struct First
{
    template <typename... Targs>
    First(Targs&&... args) {}
};

template <>
struct First<void> {};

template <typename T>
struct Second
{
    template <typename... Targs>
    Second(Targs&&... args) {}
};

template <typename... T> class A;

template <typename SecondType>
class A<SecondType>
{
  public:
    A(const A&) = default;
    explicit A(const First<void>& first) {}
    explicit A(const Second<SecondType>& second) {}
};

template <typename FirstType, typename SecondType>
class A<FirstType, SecondType>
{
  public:
    A(const First<FirstType> & first) {}
    explicit operator A<SecondType>() const { return A<SecondType>(First<>()); }
};

int main() {
    A<int, float> a{First<int>(123)};
    A<float> b = static_cast<A<float>>(a);

    // test.cpp:41:41: error: call of overloaded ‘A(A<int, float>&)’ is ambiguous
    //    41 |     A<float> b = static_cast<A<float>>(a);
    //       |                                         ^
    // test.cpp:28:14: note: candidate: ‘A<SecondType>::A(const Second<SecondType>&) [with SecondType = float]’
    //    28 |     explicit A(const Second<SecondType>& second) {}
    //       |              ^
    // test.cpp:26:5: note: candidate: ‘constexpr A<SecondType>::A(const A<SecondType>&) [with SecondType = float]’
    //    26 |     A(const A&) = default;
    //       |     ^
    
    return 0;
}

If I call the operator directly like this: A<float> b = a.operator A<float>(); then it works fine, so I wonder if there are some rules about static_cast<> being used to invoke conversion operators I don't know about. But what I find very hard to understand is why it would even consider the explicit constructors when I have not explicitly called them in any way as far as I can tell.

I'm compiling with g++ (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0

like image 454
Richard Fabian Avatar asked Dec 22 '21 15:12

Richard Fabian


1 Answers

Although it seems like it does,

static_cast<A<float>>(a);

does not in fact first try to call a user-defined conversion function. In reality it behaves identical to an imagined declaration

A<float> temp_obj(A);

where temp_obj is an invented name for the created temporary.

As a consequence,

A<float> b = static_cast<A<float>>(a);

is, except maybe for an additional move operation, identical to

A<float> b(a);

The above form is direct-initialization.

In direct-initialization only constructors of the target class are considered. User-defined conversion functions of the argument type are not considered. In your case there are two viable candidate constructors:

explicit A(const Second<SecondType>& second);

and

A(const A&);

(The explicit on the constructor doesn't play a role for direct-initialization.)

Both of these are viable and both of these require exactly one user-defined conversion on the argument. The first one's argument is obtained trough the variadic constructor of Second<SecondType> and the second one's through the user-defined conversion function of A<int, float>.

It would seem at this point that the user-defined conversion function should not be considered, since it is explicit and the initialization of the function parameters is copy-initialization, which does not allow explicit constructors and conversion functions, but there is a specific exception to this for copy/move constructors as a result of the resolution of CWG issue 899.

This leaves us with two viable constructors, both with equally good conversion sequences. As a result the construction is ambiguous and the compiler is correct.

None of the explicit markings are relevant to this. Only making the variadic constructor of Second<SecondType> as explicit would resolve the ambiguity.


However, if you use --std=c++17 or higher, you will see that the code will compile in both Clang and GCC.

This is probably because of the mandatory copy elision that was introduced in C++17. In many situation it is now mandatory that copy/move constructors are elided where they would usually need to be called.

The new rules do not actually apply to the copy constructor that we call above, but because this may just be an oversight in the standard, there is the open CWG issue 2327 considering whether copy elision should apply in this direct-initialization as well.

It seems to me that the compilers have implemented this additional elision behavior for direct-initialization already and in such a way, that it makes the elided copy/move constructor candidate a better match in the overload resolution than the normal constructor requiring a user-defined conversion sequence.

This removes the ambiguity and only the user-defined conversion function of A<int, float> is called (with elided copy/move constructor of A<float>).

like image 105
user17732522 Avatar answered Sep 28 '22 07:09

user17732522