Must aggregate field constructor be public to use aggregate initialization in C++?
Asked Answered
A

2

8

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?

Awaken answered 8/8, 2021 at 9:13 Comment(3)
Do you think this must fail for the same reason ("initialisation of the default parameter takes place on behalf of the calling code")?Hebdomadal
@n.1.8e9-where's-my-sharem. , thanks, good catch, in your example all 3 compilers are at least share the same opinion. And in case of aggregate initializers, it is required to have public destructors, so public constructor requirement would be expected at least from the symmetry point of view. But let us see what the standard says.Awaken
Well they all do that, probably because the standard says what they should do. "The names in the default argument are looked up, and the semantic constraints are checked, at the point where the default argument appears." "Access is checked for a default argument ([dcl.fct.default]) at the point of declaration, rather than at any points of use of the default argument." I conjecture that the same should be true for default member initialisers. If the standard doesn't require that explicitly, this is an oversight that should be fixed. Anything else would be inconsistent and surprising.Hebdomadal
M
3

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

Myriad answered 9/8, 2021 at 10:39 Comment(0)
A
2

In my opinion, the GCC and CLANG are behaving correctly, but the MVSC is not, for the following reasons:

  • As you already mentioned struct B is an aggregate and so aggregate initialization takes place when using list-initialization for B (list initialization
  • Since the initializer list is empty default member initialization is used (aggregate initialization
  • Since B is a friend of A using the private constuctor of A for default member initialization is allowed

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

Albaugh answered 8/8, 2021 at 10:6 Comment(2)
Thanks, but I think in the line B{} aggregate initialization takes place. If we add private destructor to A then all compilers demonstrate it with the correct error: gcc.godbolt.org/z/3vKTe5n7cAwaken
Yes, aggregate initialization takes place but the members, that do not have an initializer clause in the initializer list, (which is the case for the member a) are initialized by their default member initializer (which is A{1} for the member a).Albaugh

© 2022 - 2024 — McMap. All rights reserved.