C++11 Aggregate Initialization For Classes With Non-Static Member Initializers

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.

C++11 default class member initialization with initializer list , simultaneously

The below syntax:

abc a = {1, MY::A};

is a list-initialization which may perform differently depending on the type being initialized. Without the non-static data member initializer (/*{123};*/), your struct is an aggregate and the case falls under [dcl.init.list]/p3:

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

However, to be an aggregate type, the following conditions must be met in C++11:

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

That is, the usage of NSDMI (non-static data member initialization) breaks the above set of rules, and as a result, an instance of this type can no longer be list-initialized.

This rule has changed in C++14, and the current wording reads [dcl.init.aggr]/p1:

An aggregate is an array or a class with

  • (1.1) no user-provided, explicit, or inherited constructors ([class.ctor]),

  • (1.2) no private or protected non-static data members ([class.access]),

  • (1.3) no virtual functions, and

  • (1.4) no virtual, private, or protected base classes ([class.mi]).

[ Note: Aggregate initialization does not allow accessing protected and private base class' members or constructors. — end note ]

Partial Aggregate Initialization and Non-static Data Member Initializer

C++98, C++03

Non-static data member initialisers (NSDMIs) do not exist; the question is inapplicable.


C++11

Well, first of all, this initialisation is invalid because your type is not an aggregate:

[C++11: 8.5.1/1]: An aggregate is an array or a class (Clause 9) with 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).

So, aggregate initialisation can't be performed here; a constructor taking an std::initializer_list would be your only way to use that initialisation syntax ([C++11: 8.5.4/3]), but you don't have one of those either.

Consequently, the entire premise of the question is flawed: it is not possible to get yourself into this state.


C++1y

In the upcoming version of the standard, the definition of aggregates has been relaxed to allow your type to be deemed an aggregate (as long as both of those members stay public!):

[n3936: 8.5.1/1] 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).

Following on from this, there's a rule that guarantees the result you're looking for:

[n3936: 8.5.1/7]: If there are fewer initializer-clauses in the list than there are members in the aggregate, then each member not explicitly initialized shall be initialized from its brace-or-equal-initializer or, if there is no brace-or-equal-initializer,
from an empty initializer list (8.5.4). [ Example:

struct S { int a; const char* b; int c; int d = b[a]; };
S ss = { 1, "asdf" };

initializes ss.a with 1, ss.b with "asdf", ss.c with the value of an expression of the form int{} (that is, 0), and ss.d with the value of ss.b[ss.a] (that is, ’s’), and in

struct X { int i, j, k = 42; };
X a[] = { 1, 2, 3, 4, 5, 6 };
X b[2] = { { 1, 2, 3 }, { 4, 5, 6 } };

a and b have the same value —end example ]

Why do non-static data member initializers defeat uniform initialization syntax?

Yes, this is intended by the standard. What you are attempting here is aggregate initialization. Unfortunately, your second foo is no longer considered an aggregate due to the equal initializer of f. See 8.5.1 [dcl.init.aggr] (emphasis mine):

An aggregate is an array or a class (Clause 9) with no user-provided constructors (12.1), no brace-or-equalinitializers 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).

Because you have an equal initializer for the member f, you will need to provide a custom constructor to support the syntax you are after:

struct foo
{
int i;
float f = 0;
constexpr foo(int i, float f) : i(i), f(f) { }
};
...
foo bar{ 5, 3.141f }; // now okay

As to why this was specified in the standard, I have no idea.

Initializing a struct with aggregate initialization and member initializers

Bjarne Stroustrup and Richard Smith raised an issue about aggregate initialization and member-initializers not working together.

The definition of aggregate is slightly changed in C++11 & C++14 standard.

From the C++11 standard draft n3337 section 8.5.1 says that:

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

But C++14 standard draft n3797 section 8.5.1 says that:

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

So, when you use in class member initializer (i.e. equal initializer) for the data member id in C++11 it no longer remains aggregate & you can't write ABC abc{"hi", 0}; to initialize a struct ABC. Because it no longer remains aggregate type after that. But your code is valid in C++14. (See live demo here).

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.

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 ]

Non-static data member initialization

Given the first definition, if you create an instance of Foo with automatic storage duration, a will be uninitialized. You can perform aggregate initialization to initialize it.

Foo f{0};  // a is initialized to 0

The second and third definitions of Foo will both initialize the data member a to 0.

In C++11, neither 2 nor 3 are aggregates, but C++14 changes that rule so that they both remain aggregates despite adding the brace-or-equal-initializer.



Related Topics



Leave a reply



Submit