Document #: | P3110R0 |
Date: | 2024-02-04 |
Project: | Programming Language C++ |
Audience: |
Evolution |
Reply-to: |
James Touton <bekenn@gmail.com> |
This paper introduces array element initializer patterns, which allow
for the initialization of array elements using pattern expansion similar
to pack expansion. This is useful when initializing an array of
non-default-constructible type
T
, or when non-default
initialization is desired and explicit array initialization syntax is
too cumbersome or impossible (such as in a generic context, where the
length of the array may depend on a template argument).
Example:
int a[57] = { 5 ... }; // initializes every element of a as 5
Initialization of array elements presently requires explicit syntax for each element when the desired initialization is more complicated than default or value initialization. If the array is large, this requirement becomes burdensome to the point that the developer may need to turn to alternative data structures that allow for initialization of new elements in a separate step after the data structure itself has already been initialized:
class E
{
public:
(int);
E};
[100]; // error: E is not default constructible
E a[100] = { }; // error: E is not default constructible
E b[100] = { 0, 0, 0, /* 97 more zeros... */ }; // OK, but burdensome
E c
template <size_t N>
void f()
{
// error unless N matches the number of element initializers
[N] = { 0, 0, 0, /* how many zeros here? */ };
E x}
As an aggregate, std::array
suffers from the same limitations as built-in arrays, making it unwieldy
to use with non-default-constructible types.
This feature is intended to follow the same general rules and syntax as pack expansion. Each is an example of pattern expansion, but whereas pack expansion is governed by the size of a parameter pack, expansion of array initializer patterns is governed by the size of the target array.
initializer-list:
initializer-clause...
opt
initializer-list,
initializer-clause...
opt
The syntax for initializer lists does not change. Whereas an ellipsis was previously only permitted when an initializer-clause contained the name of an unexpanded parameter pack, an ellipsis may now also appear after the final intializer-clause to designate that initializer-clause as an array element initializer pattern. (If the initializer-clause contains the name of an unexpanded parameter pack, the meaning does not change; this is still a pack expansion rather than an array element initializer pattern.)
If the initializer pattern is (or ends with) a numeric literal, the ellipsis must be preceded by whitespace. This is because the characters making up the ellipsis would otherwise be interpreted as part of the numeric literal, even though the resulting literal would be invalid.
Example:
int a[27] = { 5... }; // error, 5...
interpreted as an invalid numeric literal
int b[27] = { 5 ... }; // OK
int c[27] = { (5)... }; // OK, pattern does not end with a numeric literal
An array element initializer pattern may appear only as the final initializer-clause in an initializer-list that initializes an array of known bound. This works for both braced initializer-lists and parenthesized expression-lists. The pattern is replicated as many times as is necessary to explicitly initialize each element of the array.
It is permitted for an initializer list to contain both ordinary initializers and a terminating initializer pattern:
// This creates an array starting with the elements 1, 2, 3, 4 and ending with
// 96 repititions of the value 5:
int a[100] = { 1, 2, 3, 4, 5 ... };
Each initializer produced from a pattern is evaluated separately, exactly as if all initializers had been written out explicitly. There are no new restrictions on the content of an initializer clause.
For example, the pattern may contain a function call, which will be evaluated separately for every element:
#include <cassert>
#include <cstddef>
int array_elem(std::size_t index);
void f()
{
::size_t n = 0;
stdint a[32] = { array_elem(n++)... };
assert(n == 32);
}
Since the expanded initializers are evaluated left-to-right for both braced and parenthesized initialization, side effects are evaluated in order of increasing element index:
#include <generator>
::generator<int> digits_of_pi();
std
void f()
{
auto g = digits_of_pi();
auto i = g.begin();
int pi_100[100](*i++...);
}
The rules for aggregate initialization (9.4.2
[dcl.init.aggr])
permit inner braces to be elided when initializing members of a
subaggregate. All known implementations of
std::array
take advantage of
this feature to meet the standard’s requirement that a
std::array
“can be
list-initialized with up to N
elements whose types are convertible to
T
” (24.3.7.1
[array.overview]).
The implementation strategy (which is not mandated or even suggested by
the standard, but which appears to be the only approach feasible without
invoking specialized compiler-specific behavior) is to use a built-in
array as the sole data member of
std::array
:
namespace std {
template<class T, size_t N>
struct array {
/* non-data members */
[N];
T _Elems};
}
With brace elision, users may initialize a
std::array
with the same form of
braced initializer as can be used to initialize a built-in array:
::array<int, 5> x = { 1, 2, 3, 4, 5 }; std
…which, given the implementation strategy for
std::array
, is equivalent to the
fully-braced form:
::array<int, 5> x = { { 1, 2, 3, 4, 5 } }; std
The fully-braced form is not sanctioned by the standard.1
In principle, std::array
could instead be implemented using a compiler-specific extension to make
initialization more consistent with a built-in array, but it is clear
that this feature must work for existing implementations of
std::array
.
To meet that need, an array element initializer pattern is permitted to appear in a braced initializer list wherein all initializers in the list appertain to elements of the same array:
struct A
{
int a, b, c[20];
};
= { 1, 2, { 3, 4, 5 ... } }; // OK
A a1 = { 1, 2, 3, 4, 5 ... }; // error: some initializers appertain to
A a2 // non-array elements
struct B
{
int x[20];
};
= { { 1, 2, 3, 4, 5 ... } }; // OK
B b1 = { 1, 2, 3, 4, 5 ... }; // OK, all initializers appertain to
B b2 // elements of the same array
struct C
{
int a[20], b, c;
};
= { { 1, 2, 3 ... }, 4, 5 }; // OK
C c1 = { 1, 2, 3 ..., 4, 5 }; // error: initializer pattern cannot appear
C c2 // in the middle of an initializer list
= { 1, 2, 3 ... }; // OK, b and c initialized to 0
C c3
struct D
{
int a, b;
};
[5] = { { 1, 2 }, { 3, 4 }... }; // OK
D d1[5] = { 1, 2, 3, 4 ... }; // error: initializers do not appertain to
D d2// elements of an array
[5] = { 1, 2, { 3, 4 }... }; // error: some initializers do not
D d3// appertain to elements of an array
C++20 introduced the ability to initialize an aggregate using a parenthesized expression-list. Arrays are aggregates, so that means arrays can be initialized with parentheses instead of braces.2 This form of initialization has no equivalent to brace elision, so the addition of initializer patterns requires no special considerations.
[CWG2149] (currently unresolved) points out an inconsistency in the wording with respect to array lengths inferred from braced initializer lists in the presence of brace elision. [P3106R0] attempts to resolve this issue by reformulating the rules for brace elision. Since this feature intersects with brace elision, the wording changes shown here are presented relative to [N4971] as modified by [P3106R0], under the assumption that [P3106R0] will be accepted.
Modify §7.6.1.4 [expr.type.conv] paragraph 2:
If the initializer is a parenthesized single expression, the type conversion expression is equivalent to the corresponding cast expression (7.6.3 [expr.cast]). Otherwise, if the type is cv
void
and the initializer is()
or{}
(afterpackpattern expansion, if any), the expression is a prvalue of typevoid
that performs no initialization. […]
Modify §9.4.1 [dcl.init.general] paragraph 18:
An initializer-clause followed by an ellipsis is a
packpattern expansion (13.7.4 [temp.variadic]).
Insert a new paragraph after §9.4.1 [dcl.init.general] paragraph 18:
A pattern expansion that is not a pack expansion is permitted to appear as the final element in a parenthesized expression-list that is used to initialize an array of known bound. Instantiation of the pattern expansion results in zero or more instantiations of the pattern such that the total number of elements in the expression-list matches the array bound.
Insert a new paragraph after §9.4.2 [dcl.init.aggr] paragraph 14 (as modified by [P3106R0]):
A pattern expansion that is not a pack expansion is permitted to appear as the final element in a brace-enclosed initializer-list if all other elements of the initializer-list appertain to elements of the same array
u
, which shall be an array of known bound, and if the pattern would also appertain to an element ofu
(disregarding the array bound). Instantiation of the pattern expansion results in zero or more instantiations of the pattern such that the total number of elements in the initializer-list matches the array bound ofu
.
Modify §9.12.1 [dcl.attr.grammar] paragraph 4:
In an attribute-list, an ellipsis may appear only if that attribute’s specification permits it. An attribute followed by an ellipsis is a
packpattern expansion (13.7.4 [temp.variadic]). […]
Modify §13.7.4 [temp.variadic] paragraph 5:
5 A
packpattern expansion consists of a pattern and an ellipsis, the instantiation of which produces zero or more instantiations of the pattern in a list (described below). The form of the pattern depends on the context in which the expansion occurs.PackPattern expansions can occur in the following contexts:[…]
Modify §13.7.4 [temp.variadic] paragraph 7:
A pattern expansion is a pack expansion if the pattern names one or more packs that are not expanded by a nested pattern expansion; such packs are called unexpanded packs in the pattern. A pack whose name appears within the pattern of a pack expansion is expanded by that pack expansion. An appearance of the name of a pack is only expanded by the innermost enclosing pack expansion.
The pattern of a pack expansion shall name one or more packs that are not expanded by a nested pack expansion; such packs are called unexpanded packs in the pattern.All of the packs expanded by a pack expansion shall have the same number ofarguments specifiedelements. An appearance of a name of a pack that is not expanded is ill-formed.[ Example: […] — end example ]
Modify §13.7.4 [temp.variadic] paragraph 8:
The instantiation of a
packpattern expansion considers itemsE
1,E
2, . . . ,E
N,;wherefor pack expansions, N is the number of elements inthe pack expansion parameterseach unexpanded pack in the pattern. EachE
i is generated by instantiating the pattern and replacing each unexpanded packexpansion parameterin the pattern withitsthe ith element of the pack. Such an element, in the context of the instantiation, is interpreted as follows:[…]
When N is zero, the instantiation of a
packpattern expansion does not alter the syntactic interpretation of the enclosing construct, even in cases where omitting thepackpattern expansion entirely would otherwise be ill-formed or would result in an ambiguity in the grammar.
Modify §13.7.4 [temp.variadic] paragraph 14:
The instantiation of any other
packpattern expansion produces a list of elementsE
1,E
2, . . . ,E
N.[ Note: The variety of list varies with the context: expression-list, base-specifier-list, template-argument-list, etc. — end note ]
When N is zero, the instantiation of the expansion produces an empty list.
[ Example: […] — end example ]
Add a new paragraph at the end of §13.7.4 [temp.variadic]:
If a pattern expansion that is not a pack expansion appears in a context that is not explicitly permitted, the program is ill-formed.
Modify §13.8.3.3 [temp.dep.expr] paragraph 6:
A braced-init-list is type-dependent if any element is type-dependent or is a pack expansion, or if the final element is a pattern expansion and all elements of the braced-init-list appertain to the same array of known bound where the bound is dependent on a template parameter.
Add an entry to §15.11 [cpp.predefined] table 22 [cpp.predefined.ft]:
Macro name Value__cpp_array_elem_pattern_init
⟨YYYYMMDD⟩L
[ Editor's note: The value of the macro is to be determined at the editor’s discretion. ]
This didn’t stop older compilers from recommending the use of the fully-braced form, which has led to an awkward situation where fully-braced initializers are common in user code despite being non-conformant with the standard. Current compilers do not warn against use of the fully-braced form.↩︎
std::array
is also an aggregate, but because the standard doesn’t mandate an
implementation strategy, there is no conforming way to initialize
std::array
with parentheses.↩︎