Search code examples
c++language-lawyercopy-constructorambiguous

Ambiguity in constructing a class from a child class


This question was inspired by What is wrong with the inheritance hierarchy in my example?

Consider we have a struct B, which can be constructed from const reference to another struct A. Can B be constructed from an object that inherits both A and B?

struct A {};

struct B {
    B() {}
    B(const A&) {}
};

struct C : A, B {};

B b1(C{}); // 1. ok everywhere
C c;
B b2(c);   // 2. ok in MSVC, error in GCC and Clang

The first option with construction from temporary B b1(C{}); is accecpted by all compilers. And the second option with construction from an lvalue B b2(c); is accepted only by MSVC, while both GCC and Clang reject it with the ambiguity error:

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

Online demo: https://godbolt.org/z/hTM6c5M73

Are GCC and Clang really correct in accepting case 1? And is it correct that they change its mind if one additionly declares default copy constructor in B:

B(const B&) = default;

Online demo: https://godbolt.org/z/9j9xeqY3r


Solution

  • B b2(c); is ambiguous because B has two viable constructors:

    B(const A&);
    B(const B&); // implicitly-declared copy constructor
    // implicitly-declared move constructor not viable
    

    Both bind the c lvalue directly to their respective parameters, but with rank of a derived-to-base conversion sequence (rather than the identity sequence) per [over.ics.ref]/1. The conversion sequences are indistinguishable in terms of best overload, because neither is A derived from B nor is B derived from A. None of the rules in [over.ics.rank]/3 and [over.ics.rank]/4 distinguish them.


    B b1(C{}); is unambiguous, because there is another viable overload

    B(B&&); // implicitly-declared move constructor
    

    Again, the reference binding is a considered a derived-to-base conversion sequence, i.e. the conversion rank is the same as before.

    But because the argument is bound directly to a rvalue reference, it is considered better than the other two overloads which only bind directly to lvalue references per [over.ics.rank]/3.2.3.


    Adding B(const B&) = default; modifies the overload set by removing the B(B&&); move constructor, because it won't be implicitly-declared if there is a user-declared copy constructor per [class.copy.ctor]/8.1.

    Then B b1(C{}); is ambiguous between the other two remaining overloads as before.