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