c++c++20designated-initializer

Does designated initializer of sub-aggregate require curly braces?


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?


Solution

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