What Is Aggregate Initialization

what is aggregate initialization

First of all, to answer the main question, aggregate initialization means the use of brace-enclosed initializer lists to initialize all members of an aggregate (i.e. an array or struct [in C++, only certain types of structs count as aggregates]).

Obviously,

int ar[] = { 1 , 2 };

is safer than

int ar[2];
ar[0] = 1;
ar[1] = 2;

because the latter gives ample opportunity for typos and other errors in the indices of the individual elements to be initialized.

Looking at today's C and C++, it's unclear to me why the author makes a distinction between C and C++. Both languages enable aggregate initialization for arrays.

One possibility is that the author referred to old versions of the C Standard. Notably, in ANSI C (C89) an important restriction applied to the use of aggregate initialization: All initializers had to be constant expressions:

/* This is possible in C89: */
f(int i)
{ int ar[] = { 1 , 2 }; }

/* But this is not
(because i is not a constant expression):
*/
f(int i)
{ int ar[] = { i , i+1 }; }

This is due to 3.5.7 in C89 (quoting from the draft I found here):

All the expressions in an initializer for an object that has static storage duration or in an initializer list for an object that has aggregate or union type shall be constant expressions.

This clearly limits the usefulness of aggregate initialization (and even in 1989, I believe many compilers implemented extensions to enable aggregate initialization also for non-constant expressions).

Later versions of the C Standard did not have this restriction, and the standardized versions of C++ (starting with C++98), I believe, never had any such restriction.

I can only speculate, but perhaps this is what the author had in mind?

Why does aggregate initialization not work anymore since C++20 if a constructor is explicitly defaulted or deleted?

The abstract from P1008, the proposal that led to the change:

C++ currently allows some types with user-declared constructors to be initialized via aggregate initialization, bypassing those constructors. The result is code that is surprising, confusing, and buggy. This paper proposes a fix that makes initialization semantics in C++ safer, more uniform,and easier to teach. We also discuss the breaking changes that this fix introduces.

One of the examples they give is the following.

struct X {
int i{4};
X() = default;
};

int main() {
X x1(3); // ill-formed - no matching c’tor
X x2{3}; // compiles!
}

To me, it's quite clear that the proposed changes are worth the backwards-incompatibility they bear. And indeed, it doesn't seem to be good practice anymore to = default aggregate default constructors.

Aggregate initialization of a union in C++ with `{}`

Clang is correct, GCC is wrong

As per [dcl.init.aggr]/1:

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

A, B and U are all aggregate classes, although the prior to are non-union aggregate classes, which the former does not qualify as.

As per [dcl.init.aggr]/5 [emphasis mine]:

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.
  • (5.2) Otherwise, if the element is not a reference, the element is copy-initialized from an empty initializer list ([dcl.init.list]).
  • (5.3) Otherwise, the program is ill-formed.

If the aggregate is a union and the initializer list is empty, then

  • (5.4) if any variant member has a default member initializer, that member is initialized from its default member initializer;
  • (5.5) otherwise, the first member of the union (if any) is copy-initialized from an empty initializer list.

Thus

U u{};

is aggregate initialization, with the result that the first data member of the union class, namely the data member a of type A (which is a non-union aggregate class), is copy-initialized from an empty initializer list. As the single data member x of the type A has a default member initializer, then as per [dcl.init.aggr]/5.1 above, the data member x is initialized by its default member initializer.

Thus, Clang is correct, and GCC is wrong.



GCC bug report

  • Bug 102013 - Incorrect aggregate initialization of union

Can C++ aggregate initialization be used to construct an instance of a class which implements an interface?

You need to initialize the base class even though it is empty:

Foo f({{},1,2});

see it live on godbolt

Further down in the standard in the section you are referring to we can see an example of this in [dcl.init.aggr]p4.2:

struct base1 { int b1, b2 = 42; };
struct base2 {
base2() {
b3 = 42;
}
int b3;
};

struct derived : base1, base2 {
int d;
};

derived d1{{1, 2}, {}, 4};
derived d2{{}, {}, 4};

initializes d1.b1 with 1, d1.b2 with 2, d1.b3 with 42, d1.d with 4,
and d2.b1 with 0, d2.b2 with 42, d2.b3 with 42, d2.d with 4. —end
example]

Also see [dcl.init.aggr]p2 which explains what the elements of an aggregate are:

The elements of an aggregate are:

-for an array, the array elements in increasing subscript order, or

-for a class, the direct base classes in declaration order, followed by the direct non-static data members ([class.mem]) that are not
members of an anonymous union, in declaration order.

and [dcl.init.aggr]p3 says:

When an aggregate is initialized by an initializer list as specified in [dcl.init.list], the elements of the initializer list are taken as initializers for the elements of the aggregate. ...

Note, the answer assumes C++17 or greater since before C++17 an aggregate was not allowed to have a base class.

Is this aggregate initialization or reference-initialization (revisted)?

Does this is an aggregate initialization or reference initialization?

A is an aggregate and A a{ B() } is list initialization according to the following rule(s):

The effects of list-list-initialization of an object of type T are:

  • If T is an aggregate class and the braced-init-list has a single element of the same or derived type (possibly cv-qualified), the object is initialized from that element (by copy-initialization for copy-list-initialization, or by direct-initialization for direct-list-initialization).

  • Otherwise, if T is a character array and the braced-init-list has a single element that is an appropriately-typed string literal, the array is initialized from the string literal as usual.

  • Otherwise, if T is an aggregate type, aggregate initialization is performed.

(emphasis mine)

Note in the above, we do not reach bullet 3 as bullet 1 is satisfied and so used.

This means that the object A is initialized from the single element B() using direct-initialization. This in turn means that the copy constructor A::A(const A&) will be used.

Here, the parameter const A& of the copy ctor A::A(const A&) can be bound to a B object so this works without any problem.



why this is not an aggregate initialization even though the class A is an aggregate class?

Because to do aggregate initialization bullet 3 here should be reached and satisfied but we never reach bullet 3 because bullet 1 is satisfied.

Does C++ zero-initialise unspecified values during aggregate initialization?

Yes. C++ generally maintains backward compatibility with C, allowing you to include and use C code. Consider if you have some legacy C code, that attempts to initialize an array like you describe:

int arr[7] = {0};

If C++ worked any differently, and the C program (validly, under C) assumed that this array is zero initialized, then the code is liable to fail if included in a C++ project compiled with a C++ compiler.

Just to confirm, I compiled this C++ program using Cygwin g++ on x64 Windows:

int main() {
int arr[7] = {0};
}

and then disassembled function main in GDB:

push   %rbp
mov %rsp,%rbp
sub $0x40,%rsp
callq 0x1004010d0 <__main>
movq $0x0,-0x20(%rbp)
movq $0x0,-0x18(%rbp)
movq $0x0,-0x10(%rbp)
movl $0x0,-0x8(%rbp)
mov $0x0,%eax
add $0x40,%rsp
pop %rbp
retq

As you can see, the program moves 3 qwords and 1 dword worth of zeros into the stack. That's 28 bytes, which is the size of 7 ints on my system.

Why does inheriting constructors break aggregate initialization?

Since C++17, aggregates can have base classes, so that for such structures being derived from other classes/structures list initialization is allowed:

struct MoreData : Data {
bool done;
};

MoreData y{{"test1", 6.778}, false};

In C++17 an aggregate is defined as

  • either an array
  • or a class type (class, struct, or union) with:
    • no user-declared or explicit constructor
    • no constructor inherited by a using declaration
    • no private or protected non-static data members
    • no virtual functions
    • no virtual, private, or protected base classes

For more details you can refer to Chapter 4 Aggregate Extensions from C++17 - The Complete Guide By Nicolai M. Josuttis

C++11 aggregate initialization for classes with non-static member initializers

In C++11 having in-class member initializers makes the struct/class not an aggregate — this was changed in C++14, however. This is something I found surprising when I first ran into it, the rationale for this restriction is that in-class initializers are pretty similar to a user defined constructor but the counter argument is that no one really expects that adding in-class initializers should make their class/struct a non-aggregate, I sure did not.

From the draft C++11 standard section 8.5.1 Aggregates (emphasis mine going forward):

An aggregate is an array or a class (Clause 9) with no user-provided
constructors (12.1), no brace-or-equal initializers for non-static
data members
(9.2), no private or protected non-static data members
(Clause 11), no base classes (Clause 10), and no virtual functions
(10.3).

and in C++14 the same paragraph reads:

An aggregate is an array or a class (Clause 9) with no user-provided
constructors (12.1), no private or protected non-static data members
(Clause 11), no base classes (Clause 10), and no virtual functions
(10.3).

This change is covered in N3605: Member initializers and aggregates which has the following abstract:

Bjarne Stroustrup and Richard Smith raised an issue about aggregate
initialization and member-initializers not working together. This
paper proposes to fix the issue by adopting Smith's proposed wording
that removes a restriction that aggregates can't have
member-initializers
.

This comment basically sums up the reluctance to allowing them to be aggregates:

Aggregates cannot have user-defined constructors and
member-initializers are essentially some kind of user-defined
constructor (element)
(see also Core Defect 886). I'm not against this
extension, but it also has implications on what our model of
aggregates actually is. After acceptance of this extension I would
like to know how to teach what an aggregate is.

The revised version N3653 was adopted in May 2013.

Update

emsr points out that G++ 5.0 now supports C++14 aggregates with non-static data member initializers using either std=c++1y or -std=c++14:

struct A { int i, j = i; };
A a = { 42 }; // a.j is also 42

See it working live.

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

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



Related Topics



Leave a reply



Submit