Why Isn't Rvo Applied to Base Class Subobject Initialization

Why isn't RVO applied to base class subobject initialization?

According to Richard Smith:

This is a defect in the standard wording. Copy elision cannot be guaranteed when initializing a base class subobject, because base classes can have different layout than the corresponding complete object type.

Is it possible to initialize member variable (or base class) of a non-copyable type?

So, I think I found the relevant parts of the standard and I think the compilers are in error regarding to X. (All links are to a standard draft so very maybe it was different in C++17, I will check that later. But gcc10 and clang10 also fail with -std=c++20, so that is not that important).

Regarding the initialization of base classes (emphasis mine): class.base.init/7

The expression-list or braced-init-list in a mem-initializer is used to initialize the designated subobject (or, in the case of a delegating constructor, the complete class object) according to the initialization rules of [dcl.init] for direct-initialization.

I think this tells us, that X() : S(foo()) {} should not be different from S s = foo(), but let's look at dcl.init/17.6.1

If the initializer expression is a prvalue and the cv-unqualified version of the source type is the same class as the class of the destination, the initializer expression is used to initialize the destination object. [Example: T x = T(T(T())); calls the T default constructor to initialize x. — end example]

This implies to me, that X() : S(foo()) {} should call the default constructor. I also tested (to be completely in line with the example) X() : S(S()) {} and this also fails on clang and g++. So it seems to me that the compilers have a defect.

When can a base class have a different layout than the corresponding complete object type?

Whenever virtual inheritance is involved.

Example:

struct A {
int a;
A (int a) : a(a) {}
};

struct B: virtual A {
B() : A(0) {}
};

B makeB { return B(); }

struct C : B {
C() : B(makeB()), A(42) {}
};

The C constructor initialises its A subobject, so the B constructor cannot. How would makeB know whether it should initialise A?

Copy elision is still theoretically possible in this case. The implementation would need to transparently create two binary versions of makeB, or add an invisible argument to makeB (i.e. use a technique employed for constructors themselves) so that it could make B with or without initialising A. This would seem to require an incompatible change in the ABI however.

There may or may not be a defect in the standard. This situation probably was not foreseen by the committee. If it was, I would be happy to read the discussion, as it surely must have left a paper trail. So the intent is unclear until there's a clarification from the committee. If the intent is to require copy elision in this case, ABI incompatibility be damned, then further changes to the standard might be required (unless the committee had foreseen the situation and made sure everything is compatible with it, in which case there should be some kind of paper trail again).

Is copy/move elision allowed to make a program using deleted functions well-formed?

It doesn't make an ill-formed program build. It gets rid of the reference to the deleted function entirely. The appropriate wording in the proposal is here:

[dcl.init] bullet 17.6

If the initializer expression is a prvalue and the cv-unqualified
version of the source type is the same class as the class of the
destination, the initializer expression is used to initialize the
destination object. [ Example: T x = T(T(T())); calls the T default
constructor to initialize x. ]

The example further strengthens this. Since it indicates the whole expression must collapse into a single default construction.

The thing to note is that the deleted function is never odr-used when the copies are elided due to value categories, so the program is not referring to it.

This is an important distinction, since the other form of copy elision still odr-uses the copy c'tor, as described here:

[basic.def.odr]/3

... A constructor selected to copy or move an object of class type is
odr-used even if the call is actually elided by the implementation
([class.copy] ...

[class.copy] describes the other form of permissible (but not mandatory) copy-elision. Which, if we demonstrate with your class:

Thing foo() {
Thing t;
return t; // Can be elided according to [class.copy.elision] still odr-used
}

Should make the program ill-formed. And GCC complains about it as expected.


And by the way. If you think the previous example in the online compiler is a magicians trick, and GCC complains because it needs to call the move c'tor. Have a look at what happens when we supply a definition.



Related Topics



Leave a reply



Submit