In the following program, aggregate struct B
has the field a
, which is itself an aggregate. Can C++20 designated initializer be used to set its value without surrounding curly braces?
struct A { int i; };
struct B { A a; };
int main() {
[[maybe_unused]] B x{1}; //ok everywhere
[[maybe_unused]] B y{.a = {1}}; //ok everywhere
[[maybe_unused]] B z{.a = 1}; //ok in MSVC,Clang; error in GCC
}
MSVC and Clang compilers accept this code. But GCC issues a weird error:
error: 'A' has no non-static data member named 'a'
Demo: https://gcc.godbolt.org/z/65j1sTcPG
Is it a bug in GCC, or such initialization is not permitted by the standard?
A designated initializer, or designator, points out a particular element to be initialized. A designator list is a comma-separated list of one or more designators. A designator list followed by an equal sign constitutes a designation.
An aggregate class gives users direct access to its members and has special initialization syntax. A class is an aggregate if. • All of its data members are public. • It does not define any constructors. • It has no in-class initializers (§ 2.6.1, p.
If a class has non-default constructors, the order in which class members appear in the brace initializer is the order in which the corresponding parameters appear in the constructor, not the order in which the members are declared (as with class_a in the previous example).
Designated initializers are the primary initializers for a class. A designated initializer fully initializes all properties introduced by that class and calls an appropriate superclass initializer to continue the initialization process up the superclass chain.
When a union is initialized by aggregate initialization, only its first non-static data member is initialized.
Each direct non-static data member named by the designated initializer is initialized from the corresponding brace-or-equals initializer that follows the designator. Narrowing conversions are prohibited. Designated initializer can be used to initialize a union into the state other than the first. Only one initializer may be provided for a union.
Until C++11, aggregate initialization could only be used in variable definition, and could not be used in a constructor initializer list, a new-expression, or temporary object creation due to syntax restrictions.
no user-provided, inherited, or explicit constructors (explicitly defaulted or deleted constructors are allowed) The effects of aggregate initialization are:
TLDR; GCC is right, everyone else is wrong because they're pretending that designated initializer-lists act like equivalent non-designated initializer-lists all the time.
To understand what is happening here (and why compilers disagree), let's look at your first example:
B x{1};
Since we're using braces, the rules of list initialization kick in. The list is not a designated initializer list, so 3.1 fails. 3.2 fails because int
is not of type B
or a type derived from B
. 3.3 fails fails because B
isn't an array of characters. Finally 3.4 is followed, which takes us to aggregate initialization.
[dcl.init.aggr]/3.2 tells us that the explicitly initialized elements of B
consist of B::a
.
Paragraph 4 tells us how the explicitly initialized elements are initialized. 4.1 doesn't apply, as B
is not a union
. But also... 4.2 doesn't apply because B::a
cannot be copy-initialized from 1
.
That seems like it shouldn't work. Fortunately, paragraphs 15 and 16 exist:
Braces can be elided in an initializer-list as follows. If the initializer-list begins with a left brace, then the succeeding comma-separated list of initializer-clauses initializes the elements of a subaggregate; it is erroneous for there to be more initializer-clauses than elements. If, however, the initializer-list for a subaggregate does not begin with a left brace, then only enough initializer-clauses from the list are taken to initialize the elements of the subaggregate; any remaining initializer-clauses are left to initialize the next element of the aggregate of which the current subaggregate is an element.
All implicit type conversions ([conv]) are considered when initializing the element with an assignment-expression. If the assignment-expression can initialize an element, the element is initialized. Otherwise, if the element is itself a subaggregate, brace elision is assumed and the assignment-expression is considered for the initialization of the first element of the subaggregate.
That is, if the initializer cannot initialize A
via copy-initialization, the rules of brace elision kick in. And A
can be initialize from an initializer-list of {1}
. Therefore, it is.
And this is where designated initializers have a problem. A designated-initializer-list
is not an initializer-list
. And therefore, the brace elision paragraphs do not apply.
And therefore, B z{.a = 1};
must fail.
The reason the other compilers don't catch this is likely the following. They probably implement designated initializers by stripping out the designations and inserting any default member initializers/value initialization between non-consecutive elements, then applying normal aggregate initializer rules. But that's not quite the same thing, since designated initializer lists don't participate in brace elision.
GCC is correct to reject this code: it attempts to copy-initialize z.a
from 1
([dcl.init.aggr]/4.2), which as the comments say doesn’t work. The brace elision that GCC seems to be envisioning to produce the diagnostic is, however, invalid: that just doesn’t exist for designated-initializer-lists.
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