This paper proposes allowing initializing aggregates from
a parenthesized list of values; that is, Aggr(val1, val2)
would mean the same thing as Aggr{val1, val2}
, except that
narrowing conversions are allowed. This is a language fix
for the problem illustrated in
N4462.
int a[](1, 2, 3);
Revision R1 implements supporting parenthesized initialization for aggregates including arrays, without support for designated initializers. The matter of initialization and deleted constructors is handled by separate paper(s).
From the initial EWG discussion:
After CWG review and feedback:
std::vector<std::array<int, 3>>().emplace_back(1,
2, 3)
work via language specification: 3 | 1 | 20 | 11 | 11After further CWG feedback and EWG discussion:
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”. 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.
During the discussion, it was suggested by CWG that we should even break an existing
corner case: Given struct A; struct C { operator A(); }; struct A { C c; };
,
the declaration A a(c);
currently invokes C
’s
conversion function to A
. It was suggested to change this behaviour to use
the (arguably better-matching) aggregate initialization in this case, i.e. to behave
like A a{c};
and not like A a = c;
. There was, however, no
consensus to pursue this direction, and the proposal remains a pure extension at this
point.
In San Diego, EWG only briefly revisited the latest revision of the Rapperswil work (then D0960R2), and reconfirmed the direction of modelling an invented constructor. It was explicitly requested to ensure that direct member initializers work as expected, and that the resulting initialization can be used in constant evaluation (if possible).
During CWG review in Kona, it became clear that the previously proposed “synthesized constructor” was problematic. The initially suggested constructor was (in the notation of the proposed wording):
explicit A(T
1&& t
1,
…, T
k&& t
k);
But that constructor is inappropriate, since it does not allow non-reference members to be initialized with lvalues. A fix would be to drop the rvalue-references and instead use:
explicit A(T
1t
1,
…, T
kt
k);
But even if the elements are direct-initialized with
static_cast<T
i&&>(t
i)
,
this approach would have required a mandatory additional move from the parameter variable, which
would have removed the solution further from the design goal of being “just like brace
initialization”.
In light of this, CWG chose to abandon the approach of an invented constructor and
reverted to a direct specification of the initialization of the aggregate elements
from the initializers. The issue of lifetime extension of temporaries is now addressed
by explicitly adding an exception to the lifetime rules ([class.temporary, 6.6.7]/6).
Moreover, the order of evaluation is again specified to be deterministic, in
left-to-right order, so as to not cause undue lack of exception safety compared
to brace initialization. Even though users should not specifically rely on this
order, it would have been needlessly dangerous to make the behaviour different from
that of brace initialization, and implementations would have been unlikely to
perform evaluation in any other order anyway.
A(b)
should not change.A
, A{b}
will not convert
b
to A
, unless b
is of type
A
or type derived from A
. A(b)
will convert even in other cases. This difference applies only to non-arrays.A{x, y, x}
forbids narrowing conversions,
while the new syntax A(x, y, z)
does allow narrowing.In [class.temporary, 6.6.7]/6, add a new bullet (6.?) between (6.9) and (6.10) as follows:
The exceptions to this lifetime rule are:
- A temporary object bound to a reference parameter in a function call (7.6.1.2) persists until the completion of the full-expression containing the call.
- A temporary object bound to a reference element of an aggregate of class type initialized from a parenthesized expression-list [dcl.init, 9.3] persists until the completion of the full-expression containing the expression-list.
- The lifetime of a temporary bound to the returned value […]
In [dcl.init, 9.3]/(17.5), edit as follows:
Otherwise, if the destination type is an array, theprogram is ill-formedobject is initialized as follows. Let x1, …, xk be the elements of the expression-list. If the destination type is an array of unknown bound, it is defined as having k elements. If k is greater than the size of the array, the program is ill-formed. Otherwise, the ith array element is copy-initialized with xi for each 1 ≤ i ≤ k, and value-initialized for each k < i ≤ n. For each 1 ≤ i < j ≤ n, every value computation and side effect associated with the initialization of the ith element of the array is sequenced before those associated with the initialization of the jth element.
In [dcl.init, 9.3]/(17.6.2), edit as follows:
Otherwise, if the initialization is direct-initialization, or if it is copy-initialization where the cv-unqualified version of the source type is the same class as, or a derived class of, the class of the destination, constructors are considered. The applicable constructors are enumerated (16.3.1.3), and the best one is chosen through overload resolution (16.3). The constructor so selected is called to initialize the object, with the initializer expression or expression-list as its argument(s). If no constructor applies and the destination type is not an aggregate, or the overload resolution is ambiguous, the initialization is ill-formed.
After [dcl.init, 9.3]/(17.6.3) insert a new sub-bullet (17.6.?) as follows:
Otherwise, if the destination type is a (possibly cv-qualified) aggregate classA
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, 9.3.1]. 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 withx
i for 1 ≤ i ≤ k. The remaining elements are initialized with their default member initializers, if any, and otherwise are value-initialized. For each 1 ≤ i < j ≤ n, every value computation and side effect associated with the initialization of ei is sequenced before those associated with the initialization of ej. [Note: By contrast with direct-list-initialization, narrowing conversions [dcl.init.list, 9.3.4] are permitted, designators are not permitted, a temporary object bound to a reference does not have its lifetime extended [class.temporary, 6.6.7], and there is no brace elision. [Example:— end example] — end note]struct A {
int a;
int&& r;
};
int f();
int n = 10;
A a1{1, f()}; // OK, lifetime is extended
A a2(1, f()); // well-formed, but dangling reference
A a3{1.0, 1}; // error: narrowing conversion
A a4(1.0, 1); // well-formed, but dangling reference
A a5(1.0, std::move(n)); // OK
In [cpp.predefined, 14.8] Table 17, add the following feature test macro:
__cpp_aggregate_paren_init 201902L
Great many thanks to the members of the Core Working Group for their thorough and diligent work across many rounds of wording review in Rapperswil and in Kona, and to Tomasz Kamiński for valuable feedback.