Please consider the code with aggregate struct B
having a field of class A
with a private constructor:
class A { A(int){} friend struct B; };
struct B { A a{1}; };
int main()
{
B b; //ok everywhere, not aggregate initialization
//[[maybe_unused]] B x{1}; //error everywhere
[[maybe_unused]] B y{}; //ok in GCC and Clang, error in MSVC
}
My question is about aggregate initialization of B
. Since the initialization takes place on behalf of the calling code (main
function here), I expected that it must be denied by the compiler, since A
's constructor is private. And indeed the construction B{1}
fails in all compilers.
But to my surprise the construction B{}
is accepted by both GCC and Clang, demo: https://gcc.godbolt.org/z/7851esv6Y
And only MSVC rejects it with the error error C2248: 'A::A': cannot access private member declared in class 'A'
.
Is it a bug in GCC and Clang, or the standard permits them to accept this code?
... 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 ]
In my opinion, the GCC and CLANG are behaving correctly, but the MVSC is not, for the following reasons:
Just for the case it was not clear for you (at least it was not clear for me). The line B x{1};
results in a compiler error, because the compiler tries to find a way to convert the integer 1 in the initializer list into an instance of A before performing copy initialization of the member a of B. But there is no way to perform that conversion at that place, since the constructor of A is private. That's the reason why you get that compiler error
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With