7

Background

Mandatory elision of copy/move operations may be already familiar to many C++ programmers. Examples copied from the linked text. Copies and moves must not be inserted by the compiler:

  • In the initialization of an object, when the initializer expression is a prvalue of the same class type (ignoring cv-qualification) as the variable type:
T x = T(T(f())); // only one call to default constructor of T, to initialize x

This can only apply when the object being initialized is known not to be a potentially-overlapping subobject:

struct C { /* ... */ };
C f();
 
struct D;
D g();
 
struct D : C
{
    D() : C(f()) {}    // no elision when initializing a base-class subobject
    D(int) : D(g()) {} // no elision because the D object being initialized might
                       // be a base-class subobject of some other class
};

This is implemented in practice by giving f a hidden extra argument telling it where to construct its return value.

Question

Why are potentially-overlapping subobjects not subject to mandatory elision? There's no reason f couldn't construct its return value directly in the base class section of D.

DXPower
  • 391
  • 1
  • 11
user253751
  • 57,427
  • 7
  • 48
  • 90
  • One reason is that any padding a the end of C may be reused by members of D. [Is tail padding of a base class guaranteed not to be reused by a derived class if the base class is an aggregate?](https://stackoverflow.com/questions/72586619/is-tail-padding-of-a-base-class-guaranteed-not-to-be-reused-by-a-derived-class-i) – BoP Aug 31 '22 at 17:45
  • @BoP The members of D haven't been initialized yet, so I still don't see the problem. – user253751 Aug 31 '22 at 18:30

1 Answers1

1

Relevant resources are

A couple of points:

  1. f() will invoke a complete object (C1) constructor, not a base object (C2) constructor. This immediately rules out copy elision for cases where C has virtual bases (in which case C1 != C2).
  2. In other cases, for a copy (materialization) to be avoided, C1 would need to unconditionally leave the tail padding untouched, unless C was a POD (in Itanium's case). However, this is impractical to guarantee, since sometimes machine instructions are more efficient when including the padding, there are ABI considerations, etc.

Your objection

The members of D haven't been initialized yet

isn't quite true, since virtual base classes are allocated at the end of the complete object:

struct A {
    char i = 5;
    A() {} // to prevent this being a POD for the purpose of layout
};
struct C {
    int j; char s;
    C() { /* ...*/ }
};
struct D : virtual A, C {
    D() : A(), C(f()) {}
};

Here, A immediately follows C in memory. We cannot know if C is going to be a base of a complete object with virtual bases later on, when generating code for f or C's ctors. If we let f construct the base object, which happens after A is constructed, we may overwrite the re-used tail padding, which is where i is located.

I recommend reading the discussion in the second link above, as it also addresses tangential points and ways in which the wording may be changed to reflect current implementation practice.

Columbo
  • 60,038
  • 8
  • 155
  • 203