Consider the following two classes:
struct Base {
Base() = default;
Base(auto&&);
};
struct Derived : Base {
using Base::Base;
};
If I try the following test with Type
= Base
and Type
= Derived
, I get different results (demo).
Type t;
const Type ct;
Type t1 = t;
Type t2 = ct;
Type t3 = std::move(t);
Type t4 = std::move(ct);
Type = Base |
Type = Derived |
|
---|---|---|
t1 |
calls forwarding reference ctor. | calls copy ctor. |
t2 |
calls copy ctor. | calls copy ctor. |
t3 |
calls move ctor. | calls move ctor. |
t4 |
calls forwarding reference ctor. | calls copy ctor. |
Why is there a difference for t1
and t4
?
So first I'll explain the "unsurprising" behaviour and then I'll go on to explain why we get the "surprising" behaviour that, in the derived case, the inherited constructor template doesn't get called in the t1
and t4
cases.
The "unsurprising" behaviour is due to the following rules:
const
reference to a non-const
value is a better implicit conversion sequence than binding a const
reference to a non-const
value ([over.ics.rank]/3.2.6), so in the t1
case, the constructor template wins.t2
case the copy constructor wins over the constructor template, and in the t3
case the move constructor wins over the constructor template.t4
case, the move constructor isn't viable because Base&&
can't bind to a const Base
value. The copy constructor is still viable (binding const Base&
to a const Base
xvalue) but the constructor template wins because the instantiated parameter type is const Base&&
, and binding an rvalue reference to an rvalue is better than binding a const lvalue reference to an rvalue ([over.ics.rank]/3.2.3)So, why don't these rules give the same results in the derived case for t1
and t4
? It's because of an obscure rule, [over.match.funcs.general]/9:
[...] A constructor inherited from class type
C
([class.inhctor.init]) that has a first parameter of type “reference to cv1P
” (including such a constructor instantiated from a template) is excluded from the set of candidate functions when constructing an object of type cv2D
if the argument list has exactly one argument andC
is reference-related toP
andP
is reference-related toD
.
To see why we have this rule, consider the following:
struct B {
B(int, int) {}
};
struct D : B {
using B::B;
};
D d(1, 2); // OK
D d(b); // error
The above is what, I think, most users would want: D
can be initialized from two int
s because it inherits that constructor from B
, but the user isn't expecting this to also allow D
to be initialized from a B
. But D
does inherit the copy constructor of B
, i.e., a constructor with a parameter type of const B&
, so we need to have another rule that prevents that inherited constructor from being used. That's what [over.match.funcs.general]/9 does. (It was added by CWG2356.)
In this case, when initializing a Derived
from a non-const lvalue of Derived
, although the inherited constructor template produces a constructor taking Derived&
, that constructor is excluded by overload resolution because C
(here, Base
) is reference-related to the referenced parameter type P
(here, Derived
) which in turn is reference-related to D
(here, Derived
). Similarly, when initializing a Derived
from a const rvalue of Derived
the constructor produced by instantiating the inherited constructor template is not a candidate. (That's also true in the t2
and t3
cases as well, although in those cases it doesn't matter, because it would lose overload resolution, like in the Base
initialization cases, even if it were a candidate.)