Document number: | P0017R1 |
Date: | 2015-10-24 |
Project: | Programming Language C++, Language Evolution Working Group |
Reply-to: | Oleg Smolsky <oleg.smolsky@gmail.com> |
C++ supports aggregate initialization, thus allowing the following succinct notation:
struct user { uint32_t id_; std::string name_; }; user u1{10, "Alice"};
The Standard states that it is possible to initialize instances of such type but places numerous restrictions (such as "no base classes"). This paper argues for an amendment to relax the rules and put more power (and syntax) into the users' hands.
The original paper, N4404 was discussed in Lenexa and the feedback was generally positive. Several EWG members expressed a strong desire to further extend the functionality, thus prompting another revision.
There are several cases revolving around inheritance of simple aggregate types that frequently come up:
Inheriting from a default-constructible base class.
Comes up when using template-based libraries such as Boost StateChart. The following is needed to define a state with a single member:
struct status_event : boost::statechart::event{ status_event(uint32 seq) : seq_(seq) {} // manually written boiler plate uint32 seq_; };
The goal here is to construct a status_event
instance while writing no boiler plate. So, lets imagine that the base
class is the first, very special element of the aggregate:
status_event event{{}, 42};
Inheriting from an aggregate type.
Comes up when people choose to inherit from an aggregate (eg C) type:
struct base { uint32 ibase_; }; struct derived : base { void f(); };
We want to contruct a derived
instance while still explicitly intializing the base clase:
derived d{{42}};
As per 8.5.1 [dcl.init.aggr], it is possible to initialize instances of simple structs and there are provisions for member structs (2), empty members (9) and even brace elision (13). However, Clause (1) states that none of this goodness works when there are base classes.
It would be really useful to extend aggregate initialization rules to cover cases where base classes are present.
Lets consider the following cases:
A single base class:
struct base { int a1, a2; }; struct derived : base { int b1; }; derived d1{{1, 2}, 3}; // full explicit initialization derived d1{{}, 1}; // the base is value initialized
The most-derived type is aggregate-initialized here and so the value b1
is reasonably obvious. The base class must be value-initialized in such a case.
Multiple base classes, mixing user-defined constructors with the compiler-provided ones.
struct base1 { int b1, b2 = 42; }; struct base2 { B() { b3 = 42; } int b3; }; struct derived : base1, base2 { int d; }; derived d1{{1, 2}, {}, 4}; // full initialization derived d2{{}, {}, 4}; // value-initialized basesThe most-derived type is aggregate-initialized, while the base classes are initialized with a mix of user-provided constructors, member initializers and value initialization.
Modify paragraph 1 (bullets added at CWG's request):
- An aggregate is an array or a class (Clause 9) with
[ Note: Aggregate initialization does not allow accessing protected and private base class' members or constructors. -end note ]
- no user-provided constructors (12.1) (including those inherited (7.3.3) from a base class),
- no private or protected non-static data members (Clause 11),
no base classes (Clause 10) and- no virtual functions (10.3), and
- no virtual, private or protected base classes (10.1).
Add a new paragraph between the existing paragraphs 1 and 2:
- The elements of an aggregate are:
-- for an array, the array elements in increasing subscript order
-- for a class, the direct base classes in declaration order followed by the direct members in declaration order
Modify the section starting with the existing paragraph 2:
- When an aggregate is initialized by an initializer list, as specified in 8.5.4, the elements of the initializer list are taken as initializers for the
memberselements of the aggregate, inincreasing subscript or memberorder. Eachmemberelement is copy-initialized from the corresponding initializer-clause. If the initializer-clause is an expression and a narrowing conversion (8.5.4) is required to convert the expression, the program is ill-formed. [ Note: If an initializer-clause is itself an initializer list, the member is list-initialized, which will result in a recursive application of the rules in this section if the member is an aggregate. -end note ] [ Example:struct A { int x; struct B { int i; int j; } b; } a = { 1, { 2, 3 } };initializes a.x with 1, a.b.i with 2, a.b.j with 3.struct base1 { int b1, b2 = 42; }; struct base2 { B() { b3 = 42; } int b3; }; struct derived : base1, base2 { int d; }; derived d1{{1, 2}, {}, 4}; derived d2{{}, {}, 4};initializes d1.b1 with 1, d1.b2 with 2, d1.b3 with 42, d1.d with 4 and d2.b1 with 0, d2.b2 with 42, d2.b3 with 42, d2.d with 4. -end example]- [no change]
- [no change]
- [no change]
- [no change]
- If there are fewer initializer-clauses in the list than there are
memberselements in the aggregate, then eachmemberelement not explicitly initialized shall be initialized from its brace-or-equal-initializer or, if there is no brace-or-equal initializer, from an empty initializer list (8.5.4).- [no change]
- If an aggregate class C contains a subaggregate
member melement e that has no members for purposes of aggregate initialization, the initializer-clause forme shall not be omitted from an initializer-list for an object of type C unless the initializer-clauses for all members of C followingme are also omitted.- [no change]
- [no change]
- 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
memberselements of a subaggregate; it is erroneous for there to be more initializer-clauses thanmemberselements. 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 thememberselements of the subaggregate; any remaining initializer-clauses are left to initialize the nextmemberelement of the aggregate of which the current subaggregate isa memberan element.- All implicit type conversions (Clause 4) are considered when initializing the
aggregate memberelement with an assignment-expression. If the assignment-expression can initializea memberan element, thememberelement is initialized. Otherwise, if thememberelement is itself a subaggregate, brace elision is assumed and the assignment-expression is considered for the initialization of the firstmemberelement of the subaggregate. [ Note: As specified above, brace elision cannot apply to subaggregates with nomemberselements for purposes of aggregate initialization; an initializer-clause for the entire subobject is required. -end note ][ Example:
struct A { int i; operator int(); }; struct B { A a1, a2; int z; }; A a; B b = { 4, a, a };
Braces are elided around the initializer-clause for b.a1.i. b.a1.i is initialized with 4, b.a2 is initialized with a, b.z is initialized with whatever a.operator int() returns. -end example ]- Note: An aggregate array or an aggregate class may contain
memberselements of a class type with a user-provided constructor (12.1). Initialization of these aggregate objects is described in 12.6.1. -end note ]- [no change]
- [no change]
- [no change]
C.4.2 Clause 3: aggregates
8.5.1
Change: definition of an aggregate is extended to apply to user-defined types with base classes.
Rationale: to increase convenience of aggregate-initialization.
Effect on original feature: It is now necessary to explicitly initialize the base as per the following example:struct derived; struct base { friend struct derived; private: base(); }; struct derived : base {}; derived d1{}; // Error. The code was well-formed before. derived d2; // still OK