Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 998
Fedor Avatar asked Aug 08 '21 09:08

Fedor


Video Answer


2 Answers

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

like image 86
dfrib Avatar answered Nov 06 '22 04:11

dfrib


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

like image 44
Fomas Avatar answered Nov 06 '22 03:11

Fomas