Copy elision when initializing a base-class subobject with aggregate initialization
Asked Answered
I

1

12

In the following code, struct B is an aggregate with base struct A, and B-object is aggregate initialized B b{ A{} }:

#include <iostream>

struct A {
  A() { std::cout << "A "; } 
  A(const A&) { std::cout << "Acopy "; } 
  A(A&&) { std::cout << "Amove "; } 
  ~A() { std::cout << "~A "; }
};

struct B : A { };

int main() {
    B b{ A{} };
}

GCC and MSVC perform A copy elision, printing:

A ~A 

while Clang creates a temporary and moves it, printing:

A Amove ~A ~A 

Demo: https://gcc.godbolt.org/z/nTK76c69v

At the same time, if one defines struct A with deleted move/copy constructor:

struct A {
  A() {} 
  A(const A&) = delete;
  A(A&&) = delete;
  ~A() {}
};

then both Clang (expected) and GCC (not so expected in case of copy elision) refuse to accept it, but MSVC is still fine with it. Demo: https://gcc.godbolt.org/z/GMT6Es1fj

Is copy elision allowed (or even mandatory) here? Which compiler is right?

There is a related question Why isn't RVO applied to base class subobject initialization? posted 4 years ago, but

  • this question askes about peculiarities of aggregate initialization, not covered there;
  • as one can see copy elision is applied in the above example by 2 out of 3 tested modern compilers. Is it due to bugs in these compilers (still present after 4 years) or they are allowed doing so?
Inigo answered 28/1, 2022 at 18:25 Comment(1)
GCC issue reported: gcc.gnu.org/bugzilla/show_bug.cgi?id=104282Inigo
B
6

Guaranteed copy elision applies here.

The aggregate initialization is described as below(N4950, dcl.init.aggr#4.2

For each explicitly initialized element:
[...]

  • Otherwise, the element is copy-initialized from the corresponding initializer-clause or is initialized with the brace-or-equal-initializer of the corresponding designated-initializer-clause.

Therefore, B b{ A {} } will copy initialize the base part from prvalue A{}. The key question is whether this copy initialization of A triggers copy elision And yes, it clearly does.

dcl.init.general#16.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 2: T x = T(T(T())); value-initializes x. — end example]

According to the standard, the compiler is expected to trigger copy elision in this case. Only MSVC is correct.

The inconsitency observed in GCC when handling this case is obviously unexpected. Bug reported by the OP. (A comment in this bug report further confirms that copy elision is indeed required in this scenario.

As mentioned in this answer, Clang doesn't consider it a bug due to the possible difference in layout between a base class and the corresponding complete object. (For a detailed discussion of on this topic, go th this SO question.) However, for aggregate classes, the layout of the base class aligns consistently with the derived class, making the reasoning less applicable. This consistency suggests that the concerns leading to the non-bug classification by Clang might not hold in the context of aggregate classes.

P.S. This is kind of a subject of CWG 2742, which states the guaranteed copy elision is well-defined only for aggregates in certain contexts. This issue is still in open status. If this principle were to be applied to all types, "different layout" problem metioned above may still be at play.

Beffrey answered 20/3 at 2:58 Comment(9)
[dcl.init.general]/16.6.1 doesn't apply because [dcl.init.general]/16.1 applies first.Kimbell
@Kimbell Thanks for pointing it out. So it should be dcl.init.general#16.1, then dcl.init.list#3.2, and finally dcl.init.general#16.6.1?Beffrey
[dcl.init.list]/3.2 doesn't apply because A is not derived from BKimbell
I'm referring to the part about initializing the base A. dcl.init.aggr#4.2 turns B's aggregate initialization into A's copy initialization. dcl.init.list#3.2 is well defined for A's initialization from A{}.Beffrey
I guess it boils down to dcl.init.general#16.6.1 either way. After re-reading the question, I now think it's the exact same issue in #46066204Kimbell
Indeed, the two questions do seem very similar. It's important to note that the question linked was raised in 2018, before the C++20 standard was officially released. There might be some differences with the latest standards that I'm not entirely certain about.Beffrey
Additionally, it's worth mentioning that this copy elision currently only applies to aggregate classes, as mentioned in this question. CWG2742 is still open, which means the answer might be different for non-aggregate classes.Beffrey
I suggest incorporating the information from #46066204 into this answer. It's probably a good idea to mention that CWG issue, and why it's a thing.Kimbell
Good idea. I’ll have it sorted in a day or so.Beffrey

© 2022 - 2024 — McMap. All rights reserved.