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.
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
Consider some type T (for simplicity, you may assume int) and some integral constant N, which we use to define an array like this:
T array[N]{}; // Note the empty braces here!
According to cppreference, value initialization is defined as follows:
This is the initialization performed when an object is constructed with an empty initializer.
But further down it is written:
In all cases, if the empty pair of braces {} is used and T is an aggregate type, aggregate-initialization is performed instead of value-initialization.
But then a little bit more down, the following statement appears:
if T is an array type, each element of the array is value-initialized;
From my understanding, the first and third quoted statements contradict to the second one.
So my two questions are:
Is the code snippet above a value initialization or an aggregate initialization?
Do the three quoted statements really contradict or am I missing something?
Note: I've seen similar questions here but they all differ a bit in the specifics.
Is T array[N]{} a value initialization or aggregate initialization?
It is list initialization and part of this initialization process involves aggregate initialization as per dcl.init.list. Additionally, it is also direct list initialization as quoted below.
1) List-initialization is initialization of an object or reference from a braced-init-list. Such an initializer is called an initializer list, and the comma-separated initializer-clauses of the initializer-list or designated-initializer-clauses of the designated-initializer-list are called the elements of the initializer list. An initializer list may be empty. List-initialization can occur in direct-initialization or copy-initialization contexts; list-initialization in a direct-initialization context is called direct-list-initialization and list-initialization in a copy-initialization context is called copy-list-initialization.
[Note 1 : List-initialization can be used
(1.1) as the initializer in a variable definition ([dcl.init])
...
— end note]
The above means that T array[N]{} is list-initialization.
Now let's move on to how the elements of the array is initialized which is given in dcl.init.list#3:
3) 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.
And since in our example T array[N] is an aggregate, the above implies that in our example the whole process of initialization of the array T array[N] involves aggregate initialization.
Finally, from aggregate initialization given below, we will note that each element is copy-initialized from an empty initializer list:
3) When an aggregate is initialized by an initializer list as specified in [dcl.init.list], the elements of the initializer list are taken as initializers for the elements of the aggregate. The explicitly initialized elements of the aggregate are determined as follows:
3.3) Otherwise, the initializer list must be {}, and there are no explicitly initialized elements.
The above means that there are no explicitly initialized elements in our example so we move onto dcl.init.aggr#5:
5) For a non-union aggregate, each element that is not an explicitly initialized element is initialized as follows:
5.2) Otherwise, if the element is not a reference, the element is copy-initialized from an empty initializer list.
(emphasis mine)
Essentially, this means that each element of type T of the array will be initialized from an empty initializer list.
Note that this also explains why the following contrived example fails in C++20:
struct T
{
T() = delete;
};
int main()
{
T array[5]{}; //this fails as a consequence of above explanation
}
These two lines from cppreference
What is the difference between these two statements ? I don't see any difference
until c++14
If the braced-init-list is empty and T is a class type with a default
constructor, value-initialization is performed. Otherwise, if T is an
aggregate type, aggregate initialization is performed.
since c++14
If T is an aggregate type, aggregate initialization is performed.
Otherwise, if the braced-init-list is empty and T is a class type with
a default constructor, value-initialization is performed.
The difference is which one happens when both conditions apply: if T is an aggregate class (as opposed to an array), which certainly has a default constructor, and the braced-init-list is empty. Of course, to understand why that matters, we then have to distinguish value initialization from aggregate initialization from an empty list.
Value initialization zero-initializes the object and then default-initializes it, which for an aggregate is default-initializing each of its members, so the value-initialization is member-wise (plus zeroing padding). Aggregate initialization initializes each member from {}, which is again value initialization for many types but is default initialization for members of class type with a user-provided default constructor. The difference can be seen in
struct A {A() {} int i;};
struct B {A a;}; // aggregate
B b{}; // i is 0 in C++11, uninitialized in C++14
B b2=B(); // i is 0 in both versions
In C++14 only, aggregates can have default member initializers; that can't contribute to a difference in behavior between the two language versions, of course, but it doesn't behave differently between these two rules anyway (since it replaces only the common default initialization).
The difference is the sequence of checking, so the aggregate type checking is taking place at the first place, and only then the rest.
C++20 adopted p0960 - allowing initialization of aggregates from a parenthesized list of values.
The exact wording ([dcl.init] 17.6.2.2) says:
[...] if no constructor is viable, the destination type is an aggregate
class, and the initializer is a parenthesized expression-list, the
object is initialized as follows.
Let e1 , …, en be the elements
of the aggregate ([dcl.init.aggr]).
Let x1, …, xk be the elements of the expression-list.
If k is greater than n, the program is ill-formed.
The element ei is copy-initialized with xi for 1
≤ i ≤ k . The remaining elements are initialized with their default
member initializers [...]
This doesn't allow initialization of inner array with parenthesized list of values:
struct Foo {
int i, j;
};
struct Moo {
int arr[2];
};
int main() {
// before C++20:
Foo foo1{1, 2};
// with C++20:
Foo foo2(1, 2); // p0960! we are good
// before C++20:
Moo moo1{1, 2};
// C++20 - oops p0960 doesn't help here:
Moo moo2(1, 2); // error: too many initializers
// before C++20:
std::array<int, 2> arr1{1, 2}; // OK
std::array<int, 2> arr2({1, 2}); // OK
std::array<int, 2> arr3{{1, 2}}; // OK
// C++20 - oops p0960 doesn't help here:
std::array<int, 2> arr4(1, 2); // error: too many initializers
}
The fact that std::array cannot be initialized with rounded brackets prevents it from participating in a generic code that creates an object of unknown type T from a list of values (e.g. an algorithm that uses make_shared, make_unique, make_from_tuple etc.).
Why p0960 didn't take a more simple approach making ()-initialization more like {}?
For example, something like:
if no constructor is viable, the destination type is an aggregate
class, and the initializer is a parenthesized expression-list, the
object would be initialized as if the values were sent with brace-initialization.
p0960 changed between r1 and r2:
r2: This revision changes the mental model from the original “literal rewrite to a braced list” to “as if a synthesized, explicit constructor with appropriate mem-initializers was called” 1. This has the effect of allowing narrowing conversions in the parenthesized list, even when narrowing conversions would be forbidden in the corresponding braced list syntax. It also clarifies the non-extension of temporary lifetimes of temporaries bound to references, the absence of brace elision, and the absence of a well-defined order of evaluation of the arguments.
The reason why this change was made can be found in the changed design principles for p0960:
r1: Parenthesized initialization and braced-initialization should be as similar as possible.
r2:
Parenthesized initialization and braced-initialization should be as similar as possible, but as distinct as necessary to conform with the existing mental models of braced lists and parenthesized lists.
(emphasis mine)
"Why p0960 didn't take a more simple approach making ()-initialization more like {}?":
When the decision to go for conformance with the existing mental models 1 had been taken, not allowing brace elision seems like the only approach.
When I use an initializer list to create a struct, but the initializer list contains fewer elements than my struct, I see the remaining elements are initialized with zeroes.
Is this an undefined behaviour and I'm seeing zeroes because my compiler (VS2015) decided to zero the memory for me?
Or could someone point me to the documentation that explains this behaviour in C++?
This is my code:
struct Thing {
int value;
int* ptr;
};
void main() {
Thing thing { 5 };
std::cout << thing.value << " " << thing.ptr << std::endl;
}
And this is what it prints:
5 00000000
That last element is the one that got zeroed without an initializer.
This is defined behaviour. According to the rule of aggregate initialization, the remaining member ptr will be value-initialized, i.e. zero-initialized to NULL pointer.
(emphasis mine)
If the number of initializer clauses is less than the number of members and bases (since C++17) or initializer list is completely empty, the remaining members and bases (since C++17) are initialized by their default initializers, if provided in the class definition, and otherwise (since C++14) by empty lists, in accordance with the usual list-initialization rules (which performs value-initialization for non-class types and non-aggregate classes with default constructors, and aggregate initialization for aggregates). If a member of a reference type is one of these remaining members, the program is ill-formed.
Is this list-initialization of an array of unknown size valid in C++0x?
int main() { int x[]{0, 1,2,3,4}; return x[0]; }
I believe it is valid, but would appreciate some confirmation.
If anyone could quote from the C++0x-FCD to support their case, it would be greatly appreciated.
Thanks!
This goes from 8.5/16 first bullet to 8.5.4 list-initialization and from 8.5.4/3 third bullet to 8.5.1 aggregate initialization and then 8.5.1/4 says
An array of unknown size initialized with a brace-enclosed initializer-list containing n initializer-clauses, where shall be greater than zero, is defined as having elements
The only difference if the object is an array between = { ... } and { ... } is that the first is called copy-list-initialization and the second is called direct-list-initialization, so both are kinds of list-initialization. The elements of the array are copy-initialized from the elements of the initializer list in both cases.
Notice that there is a subtle difference between those forms if the array has a size and the list is empty, in which case 8.5.4 second bullet applies:
struct A {
explicit A();
};
A a[1]{}; // OK: explicit constructor can be used by direct initialization
A a[1] = {}; // ill-formed: copy initialization cannot use explicit constructor
This difference does not apply to lists that have content in which case third bullet applies again, though
struct A {
explicit A(int);
};
A a[1]{0}; // ill-formed: elements are copy initialized by 8.5.1
A a[1] = {0}; // ill-formed: same.
The FCD changed this compared to the previous draft, and initialization with an empty initializer list now always works even with explicit default constructors. This is because the FCD states that the elements are value-initialized, and value initialization doesn't care about explicitness since it doesn't do overload resolution on default constructors (it couldn't figure out better or worse matches anyway). The previous draft used normal overload resolution on the constructors and thus rejected explicit default constructors during copy initialization. This defect report did that change.
Yes, it is valid, and has been for decades, even in C. The size is simply set to the number of elements supplied. I don't know the reference, unfortunately.
(Added bonus...) If you need the number of elements use sizeof(x)/sizeof(*x). It's safer than hard-coding a constant that may become invalid if you add or remove entries.
EDIT: As pointed out in the comments, the code in question is missing an = (a fact that I missed), without which it isn't valid in any current standard of C or C++.