... with aggregate struct B
...
For completeness, let's begin with noting that B
is indeed an aggregate in C++14 through C++20, as per [dcl.init.aggr]/1 (N4861 (March 2020 post-Prague working draft/C++20 DIS)):
An aggregate is an array or a class ([class]) with
- (1.1) no user-declared or inherited constructors ([class.ctor]),
- (1.2) no private or protected direct non-static data members ([class.access]),
- (1.3) no virtual functions ([class.virtual]), and
- (1.4) no virtual, private, or protected base classes ([class.mi]).
whereas in C++11, B
is disqualified as an aggregate due to violating no brace-or-equal-initializers for non-static data members, a requirement that was removed in C++14.
Thus, as per [dcl.init.list]/3 B x{1}
and B y{}
are both aggregate initialization:
List-initialization of an object or reference of type T
is defined
as follows:
- [...]
- (3.4) Otherwise, if
T
is an aggregate, aggregate initialization is performed ([dcl.init.aggr]).
For the former case, B x{1}
, the data member a
of B
is an explicitly initialized element of the aggregate, as per [dcl.init.aggr]/3. This means, as per [dcl.init.aggr]/4, particularly /4.2, that the data member is copy-initialized from the initializer-clause, which would require a temporary A
object to be constructed in the context of the aggregate initialization, making the program ill-formed, as the matching constructor of A
is private.
B x{1}; // needs A::A(int) to create an A temporary
// that in turn will be used to copy-initialize
// the data member a of B.
If we instead use an A
object in the initializer-clause, there is no need to access the private constructor of A
in the context of the aggregate initialization, and the program is well-formed.
class A {
public:
static A get() { return {42}; }
private:
A(int){}
friend struct B;
};
struct B { A a{1}; };
int main() {
auto a{A::get()};
[[maybe_unused]] B x{a}; // OK
}
For the latter case, B y{}
, as per [dcl.init.aggr]/3.3, the data member a
of B
is no longer an explicitly initialized element of the aggregate, and as per [dcl.init.aggr]/5, particularly /5.1
For a non-union aggregate, each element that is not an explicitly
initialized element is initialized as follows:
- (5.1) If the element has a default member initializer ([class.mem]), the element is initialized from that initializer.
- [...]
and the data member a
of B
is initialized from its default member initializer, meaning the private constructor A::A(int)
is no longer accessed from a context where it is not accessible.
Finally, the case of the private destructor
If we add private destructor to A
then all compilers demonstrate it with the correct error:
is governed by [dcl.init.aggr]/8 [emphasis mine]:
The destructor for each element of class type is potentially invoked ([class.dtor]) from the context where the aggregate initialization occurs. [ Note: This provision ensures that destructors can be called for fully-constructed subobjects in case an exception is thrown ([except.ctor]). — end note ]