Does designated initializer of sub-aggregate require curly braces?

Viewed 169

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?

2 Answers

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.

Related