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