Document number: P0960R3
Audience: EWG, CWG

Ville Voutilainen
Thomas Köppe
2019-02-22

Allow initializing aggregates from a parenthesized list of values

Abstract

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.

Revision history

Jacksonville 2018 discussion feedback

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

Rapperswil 2018 discussion feedback

From the initial EWG discussion:

After CWG review and feedback:

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

San Diego 2018 discussion feedback

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

Kona 2019 CWG review feedback

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(T1&& t1,, Tk&& tk);

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(T1 t1,, Tk tk);

But even if the elements are direct-initialized with static_cast<Ti&&>(ti), 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.

Design principles

Wording

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:

In [dcl.init, 9.3]/(17.5), edit as follows:

Otherwise, if the destination type is an array, the program 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 class A 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 with xi 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:
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
— end example] — end note]

In [cpp.predefined, 14.8] Table 17, add the following feature test macro:

__cpp_aggregate_paren_init 201902L

Acknowledgements

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.