15

GCC and Clang do not perform C++17’s guaranteed copy elision when a base class constructor is called; see this question and the corresponding Clang bug report for details.

In response to the bug report, Richard Smith states:

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.

Under what circumstances can a base class have a “different layout than the corresponding complete object type” in a way that makes guaranteed copy elision impossible? Is there a concrete example that illustrates this?

pycache
  • 165
  • 4
  • I'm guessing something to do with virtual inheritance. – Passer By Mar 21 '19 at 05:49
  • I would guess it has to do with vtables - and yeah, virtual inheritance. However, I'm not sure how that would be a defect in the standard. – xaxxon Mar 21 '19 at 05:52
  • @xaxxon If true, than the standard mandates something impossible. I'd call that a defect. Vptrs are invisible to the user, and wouldn't matter even if it's copied. – Passer By Mar 21 '19 at 06:00

1 Answers1

7

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

n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243
  • It requires not only ABI change. The semantics of the constructor invoked in the function would change: sometimes it would construct the virtual parent (with locally set parameters), and sometimes the virtual parent would be constructed before the function is even called. – Michael Veksler Mar 21 '19 at 07:07
  • @MichaelVeksler This is not new. There are already two variants of `B::B()`, one that initialises `A` and one that doesn't. Requiring the same thing for `makeB` is a small straightforward geberalisation. – n. m. could be an AI Mar 21 '19 at 07:20
  • I think it is a big deal, because makeB today is self-contained. Today the object that makeB returns is constructed completely from nothing to fully constructed. makeB is not aware of anything in the caller, other than the address where to construct the object. With this change, makeB will have to know if the virtual base was constructed or not. If the virtual base is already constructed, then makeB will have to avoid constructing it. This changes the semantics of makeB, which is no longer self-contained. – Michael Veksler Mar 21 '19 at 07:32
  • @MichaelVeksler This is all true but I don't see why changing this would be such a big deal. `B::B()` already has all these scary properties, and we have survived somehow. – n. m. could be an AI Mar 21 '19 at 07:36
  • This can't be done without a change in the standard. It is a big deal, since it is the first time that the behavior of the function changes according to calls it. Until now it happened only in member intializer list, where it was closer to the cause. Moving this into unrelated functions increases the surprise factor of programmers. – Michael Veksler Mar 21 '19 at 07:54
  • @MichaelVeksler The behaviour in initialiser lists is *more* surprising. The programmer wrote `A(0)`, why is `A` initialised to 42? This is governed by a single phrase in the standard "A mem-initializer where the mem-initializer-id denotes a virtual base class is ignored during execution of a constructor of any class that is not the most derived class". If copy elision is to be performed, the returned object ceases to always be the most derived object, and that's about it. Everything else follows from that. – n. m. could be an AI Mar 21 '19 at 08:15
  • Or even better, makeB should have a hidden "meta" parameter, which tells everything for makeB. The layout, the expected constructor calls, etc. Do you think it is doable? (I don't think that creating multiple `makeB`s is a good idea, as if `B` has several virtual bases, there would be a need for exponential amount of `makeB`s) – geza Mar 21 '19 at 10:04
  • @geza Yes but this is exactly the ABI change part. To remain backwards binary compatible, one needs a version of B without the hidden parameter. (It can just call the version with a hidden parameter). And you cannot be forward binary compatible in any case. – n. m. could be an AI Mar 21 '19 at 10:50
  • @geza actually when I think about this, I don't see how it's possible to remain backwards binary compatible in any case. – n. m. could be an AI Mar 21 '19 at 10:55
  • @geza No need for many versions of makeB, it either returns a most derived object (should initialise all virtual bases) or it doesn't (should skip all virtual bases). – n. m. could be an AI Mar 21 '19 at 10:57
  • oops, you're right on the "many versions of makeB". I don't know what I was thinking :) On ABI change: isn't it the only problem, that at linking stage, "unresolved symbol" occurs (the new version of `makeB`, which doesn't call virtual base constructors, is not available in old libraries)? This could be solved by a flag which instructs the compiler to behave the old way (no copy elision) during compilation. So this ABI change would not be a serious change. – geza Mar 21 '19 at 11:20
  • @geza yep, that would be exactly "use the old ABI" flag. Not serious? You bet. The cxx11 ABI change in gcc is still haunting us. This one is no different (smaller impact only because virtual inheritance is rarely used). – n. m. could be an AI Mar 21 '19 at 11:23
  • So, in conclusion, this could be done without too much hassle, right? So maybe it was a mistake to close this clang bug as "won't fix" (if the standard will keep requiring copy elision this case). Now I see your comment edit: I think it would have a much smaller impact. As you say, virtual inheritance is rarely used. Actually, I don't know any place in the standard library where it is used. – geza Mar 21 '19 at 11:30
  • @geza I'm not saying there is a defect in the standard, and I'm not saying there isn't one. A change might be needed whichever way the issue is resolved. I just don't know. VI is used in iostreams https://en.cppreference.com/w/cpp/io/basic_istream – n. m. could be an AI Mar 21 '19 at 11:45
  • @geza I added a clarification to the answer. – n. m. could be an AI Mar 21 '19 at 11:53