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