Initializer List proposed wording

Jason Merrill, Daveed Vandevoorde
Document number N2672=08-0182
Revision 22


This paper is an update of the proposed wording for the initializer list proposal in N2640=08-0150 from the pre-Sophia Antipolis mailing. The wording is strongly derived from the wording in N2531=08-0041 by J. Stephen Adamczyk, Gabriel Dos Reis, and Bjarne Stroustrup.

The changes to clauses 18 and 20 are from N2531=08-0041; the LWG felt that they belong with the core wording.

Proposed Wording

In 8.5 [dcl.init], change

    = initializer-clause
    ( expression-list )

    { initializer-list ,opt}
    { }

    initializer-clause ...opt
    initializer-list , initializer-clause ...opt

    { initializer-list ,opt }
    { }

In 5.2 [], change

    postfix-expression [ expression ]
    postfix-expression [ braced-init-list ]
    postfix-expression ( expression-listopt )
    simple-type-specifier ( expression-listopt )
    typename-specifier ( expression-listopt )
    simple-type-specifier braced-init-list
    typename-specifier braced-init-list

    assignment-expression ...opt
    expression-list , assignment-expression ...opt

In 5.3.4 [], change

    ( expression-listopt )

In 5.17 [expr.ass], change

    logical-or-expression assignment-operator assignment-expression 
    logical-or-expression assignment-operator initializer-clause 

In 6.4 [], change

    type-specifier-seq declarator = assignment-expressioninitializer-clause
    type-specifier-seq declarator braced-init-list

In 6.6 [stmt.jump], change

    return expressionopt ;
    return braced-init-list ;

In 12.6.2 [class.base.init], change

    mem-initializer-id ( expression-listopt ) 
    mem-initializer-id braced-init-list

In 8.5 [dcl.init], change paragraphs 12-15:

The initialization that occurs in the form
  T x = a;
as well as
in argument passing, function return, throwing an exception (15.1), handling an exception (15.3), and brace-enclosed initializer listsaggregate member initialization (8.5.1) is called copy-initialization. and is equivalent to the form
  T x = a;

The initialization that occurs in the forms

  T x(a);
  T x{a};
as well as in new expressions (5.3.4), static_cast expressions (5.2.9), functional notation type conversions (5.2.3), and base and member initializers (12.6.2) is called direct-initialization. and is equivalent to the form
  T x(a);

If T is a scalar type, then a declaration of the form

  T x = { a };

is equivalent to

  T x = a;

The semantics of initializers are as follows. The destination type is the type of the object or reference being initialized and the source type is the type of the initializer expression. The source type is not defined when the initializer is brace-encloseda braced-init-list or when it is a parenthesized list of expressions.

In 8.5.1 [decl.init.aggr] paragraph 2, change

When an aggregate is initialized the initializer can contain an initializer-clause consisting of a brace-enclosed , comma-separated list of initializer-clauseWhen, as specified in [dcl.init.list], an aggregate is initialized by an initializer list, the elements of the initializer list are taken as initializers for the members of the aggregate, written in increasing subscript or member order. If the aggregate contains subaggregates, this rule applies recursively to the members of the subaggregate. Each member is copy-initialized from the corresponding initializer-clause. If the initializer-clause is an expression and a narrowing conversion ([dcl.init.list]) 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: ...

In 8.5.1 paragraph 11:

In a declaration of the form
  T x = { a };
Braces can be elided in an initializer-list as follows. [Footnote: Braces cannot be elided in other uses of list-initialization.] If the initializer-list begins with a left brace, ....

In 8.5 [dcl.init], add a new section as 8.5.4 [dcl.init.list]:

8.5.4 List-initialization [dcl.init.list]

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 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: List-initialization can be used

[ Example:

  int a = {1};
  std::complex<double> z{1,2};
  new std::vector<std::string>{"once", "upon", "a", "time"}; // 4 string elements
  f( {"Nicholas","Annemarie"} ); // pass list of two elements
  return { "Norah" }; // return list of one element
  int* e {}; // initialization to zero / null pointer
  x = double{1}; // explicitly construct a double 
  std::map<std::string,int> anim = { {"bear",4}, {"cassowary",2}, {"tiger",7} };

--- end example ]--- end note ]

A constructor is an initializer-list constructor if its first parameter is of type std::initializer_list<E> or reference to possibly cv-qualified std::initializer_list<E> for some type E, and either there are no other parameters or else all other parameters have default arguments (8.3.6). [Note: Initializer-list constructors are favored over other constructors in list-initialization ([over.match.list]).] The template std::initializer_list is not predefined; if the header <initializer_list> is not included prior to a use of std::initializer_list--even an implicit use in which the type is not named ([])--the program is ill-formed.

List-initialization of an object or reference of type T is defined as follows.

  1. If T is an aggregate, aggregate initialization is performed (8.5.1 [dcl.init.aggr]). [Example:

      double ad[] = { 1, 2.0 }; // ok
      int ai[] = { 1, 2.0 }; // error: narrowing

    --- end example]

  2. Otherwise, if T is a specialization of std::initializer_list<E>, an initializer_list object is constructed as described below and used to initialize the object according to the rules for initialization of an object from a class of the same type (8.5).
  3. Otherwise, if T is a class type, constructors are considered. If T has an initializer-list constructor, the argument list consists of the initializer list as a single argument; otherwise, the argument list consists of the elements of the initializer list. The applicable constructors are enumerated ([over.match.list]) and the best one is chosen through overload resolution (13.3). If a narrowing conversion (see below) is required to convert any of the arguments, the program is ill-formed.

    [ Example:

      struct S {
        S(std::initializer_list<double>); // #1
        S(std::initializer_list<int>); // #2
        // ...
      S s1 = { 1.0, 2.0, 3.0 }; // invoke #1
      S s2 = { 1, 2, 3 }; // invoke #2

    --- end example]

    [ Example:

      struct Map {
      Map ship = {{"Sophie",14}, {"Surprise",28}}; 

    --- end example]

    [ Example:

      struct S {
        // no initializer-list constructors
        S(int, double, double); // #2
        S(); // #3
        // ...
      S s1 = { 1, 2, 3.0 };  // ok: invoke #2
      S s2 { 1.0, 2, 3 }; // error: narrowing
      S s3 { }; // ok: invoke #3
      struct S2 {
        int m1;
        double m2,m3;
      S2 s21 = { 1, 2, 3.0 }; // ok
      S2 s22 { 1.0, 2, 3 }; // error: narrowing
      S2 s23 {}; // ok: default to 0,0,0

    --- end example]

  4. Otherwise, if T is a reference type, an rvalue temporary of the type referenced by T is list-initialized, and the reference is bound to that temporary. [ Note: As usual, the binding will fail and the program is ill-formed if the reference type is an lvalue reference to a non-const type. ]

    [ Example:

      struct S {
        S(std::initializer_list<double>); // #1
        S(const std::string&); // #2
        // ...
      const S& r1 = { 1, 2, 3.0 }; // ok: invoke #1
      const S& r2 { "Spinach" }; // ok: invoke #2
      S& r3 = { 1, 2, 3 }; // error: initializer is not an lvalue

    --- end example]

  5. Otherwise (i.e., if T is not an aggregate, class type, or reference), if the initializer list has a single element, the object is initialized from that element; if a narrowing conversion (see below) is required to convert the element to T, the program is ill-formed. [ Example:

      int x1 {2}; // ok
      int x2 {2.0}; // error: narrowing

    --- end example]

  6. Otherwise, if the initializer list has no elements, the object is value-initialized; [ Example

      int** pp {}; // initialized to null pointer

    --- end example]

  7. Otherwise, the program is ill-formed.

[ Example:

  struct A { int i; int j; };
  A a1 { 1, 2 }; // aggregate initialization 
  A a2 { 1.2 }; // error: narrowing
  struct B {
  B b1 { 1, 2 }; // creates initializer_list<int> and calls constructor
  B b2 { 1, 2.0 }; // error: narrowing
  struct C {
    C(int i, double j);
  C c1 = { 1, 2.2 }; // calls constructor with arguments (1, 2.2) 
  C c2 = { 1.1, 2 }; // error: narrowing

  int j { 1 }; // initialize to 1
  int k {}; // initialize to 0

--- end example]

When an initializer list is implicitly converted to a std::initializer_list<E>, the object passed is constructed as if the implementation allocated an array of N elements of type E, where N is the number of elements in the initializer list. Each element of that array is initialized with the corresponding element of the initializer list converted to E, and the std::initializer_list<E> object is constructed to refer to that array. If a narrowing conversion is required to convert the element to E, the program is ill-formed. [ Example:

  struct X {
    X(std::initializer_list<double> v);
  X x{ 1,2,3 };

The initialization will be implemented in a way roughly equivalent to this:

  double __a[3] = {double{1}, double{2}, double{3}};
  X x(std::initializer_list<double>(__a, __a+3));

assuming that the implementation can construct an initializer_list with a pair of pointers. --- end example]

The lifetime of the array is the same as that of the initializer_list object. [Example:

  typedef std::complex<double> cmplx;
  std::vector<cmplx> v1 = { 1, 2, 3 };

  void f()
    std::vector<cmplx> v2{ 1, 2, 3 };
    std::initializer_list<int> i3 = { 1, 2, 3 };

For v1 and v2, the initializer_list object and array created for { 1, 2, 3 } have full-expression lifetime. For i3, the initializer_list object and array have automatic lifetime. --- end example ] [ Note: The implementation is free to allocate the array in read-only memory if an explicit array with the same initializer could be so allocated. --- end note]

A narrowing conversion is an implicit conversion

[Note: As indicated above, such conversions are not allowed at the top level in list-initializations. [ Example:

  int x = 999; //  x is not a constant expression
  const int y = 999;
  const int z = 99;
  char c1 = x; // ok, though it might narrow (in this case, it does narrow)
  char c2{x}; // error, might narrow
  char c3{y}; // error: narrows
  char c4{z}; // ok, no narrowing needed
  unsigned char uc1 = {5}; // ok: no narrowing needed
  unsigned char uc2 = {-1}; // error: narrows
  unsigned int ui1 = {-1}; // error: narrows
  signed int si1 = { (unsigned int)-1 }; // error: narrows
  int ii = {2.0}; // error: narrows
  float f1 { x }; // error: narrows
  float f2 { 7 }; // ok: 7 can be exactly represented as a float
  int f(int);
  int a[] = { 2, f(2), f(2.0) }; // ok: the double-to-int conversion is not at the top level

--- end example] ]

In 5.2.1 [expr.sub], add as a new paragraph 2:

A braced-init-list may appear as a subscript for a user-defined operator[]. In that case, the initializer list is treated as the initializer for the subscript argument of the operator[]. An initializer list shall not be used with the built-in subscript operator. [ Example:

  struct X {
    Z operator[](std::initializer_list<int>);
  X x;
  x[{1,2,3}] = 7; // ok: meaning x.operator[]({1,2,3})
  int a[10];
  a[{1,2,3}] = 7; // error: built-in subscript operator

--- end example]

In 5.2.3 [expr.type.conv], change:

A simple-type-specifier ( or typename-specifier (14.6) followed by a parenthesized expression-list constructs a value of the specified type given the expression list. If the expression list is a single expression, the type conversion expression is equivalent (in definedness, and if defined in meaning) to the corresponding cast expression (5.4). If the simple-type-specifier specifiestype specified is a class type, the class type shall be complete. If the expression list specifies more than a single value, the type shall be a class with a suitably declared constructor (8.5, 12.1), and the expression T(x1, x2, ...) is equivalent in effect to the declaration T t(x1, x2, ...); for some invented temporary variable t, with the result being the value of t as an rvalue.

The expression T(), where T is a simple-type-specifier ( or typename-specifier for a non-array complete object type or the (possibly cv-qualified) void type, creates an rvalue of the specified type, which is value-initialized (8.5; no initialization is done for the void() case). [ Note: if T is a non-class type that is cv-qualified, the cv-qualifiers are ignored when determining the type of the resulting rvalue (3.10). --- end note ]

Similarly, a simple-type-specifier or typename-specifier followed by a braced-init-list creates a temporary object of the specified type direct-list-initialized ([dcl.init.list]) with the specified braced-init-list, and its value is that temporary object as an rvalue.

In 5.3.4 [] paragraph 16, change part of the bullet list:

In 5.17 [expr.ass], add as a new final paragraph:

A braced-init-list may appear on the right-hand side of


  complex<double> z;
  z = { 1,2 }; // meaning z.operator=({1,2})
  z += { 1, 2 }; // meaning z.operator+=({1,2})
  a = b = { 1 }; // meaning a=b=1;
  a = { 1 } = b; // syntax error

--- end example]

In 6.4 paragraph 2:

... If the auto type-specifier appears in the type-specifier-seq, the type-specifier-seq shall contain no other type-specifiers except cv-qualifiers, and the type of the identifier being declared is deduced from the assignment-expressioninitializer as described in

In 6.6.3 [stmt.return] paragraph 2, change

A return statement without an expression can be used only in functions that do not return a value, that is, a function with the return type void, a constructor (12.1), or a destructor (12.4). A return statement with an expression of non-void type can be used only in functions returning a value; the value of the expression is returned to the caller of the function. The expression is implicitly converted to the return type of the function in which it appears. A return statement can involve the construction and copy of a temporary object (12.2). [ Note: A copy operation associated with a return statement may be elided or considered as an rvalue for the purpose of overload resolution in selecting a constructor (12.8). -- end note ] A return statement with a braced-init-list initializes the object or reference to be returned from the function by copy-list-initialization ([dcl.init.list]) from the specified initializer list. [ Example:

  std::pair<std::string,int> f(const char* p, int x)
    return {p,x};

--- end example]

Flowing off the end of a function is equivalent to a return with no value; this results in undefined behavior in a value-returning function.

In [], paragraph 6, change

Once the type of a declarator-id has been determined according to 8.3, the type of the declared variable using the declarator-id is determined from the type of its initializer using the rules for template argument deduction. Let T be the type that has been determined for a variable identifier d. Obtain P from T by replacing the occurrences of auto with either a new invented type template parameter U or, if the initializer is a braced-init-list ([dcl.init.list]), with std::initializer_list<U>. Let A be the type of the initializer expression for d. The type deduced for the variable d is then the deduced type determined using the rules of template argument deduction from a function call (, where P is a function template parameter type and Athe initializer for d is the corresponding argument type. If the deduction fails, the declaration is ill-formed. [ Example:

  auto x1 = { 1, 2 }; // decltype(x1) is std::initializer_list<int>
  auto x2 = { 1, 2.0 }; // error: cannot deduce element type

--- end example]

In 12.2 [class.temporary], paragraph 3, change

The second context is when a reference is bound to a temporary. The temporary to which the reference is bound or the temporary that is the complete object of a subobject to which the reference is bound persists for the lifetime of the reference except as specified below. A temporary bound to a reference member in a constructor's ctor-initializer (12.6.2) persists until the constructor exits. A temporary bound to a reference parameter in a function call (5.2.2) persists until the completion of the full expression containing the call. A temporary bound to the returned value in a function return statement (6.6.3) persists until the function exits. A temporary bound to a reference in a new-initializer (5.3.4 []) persists until the completion of the full-expression containing the new-initializer [Example:

  struct S { int mi; const std::pair<int,int>& mp; };
  S a { 1, {2,3} };
  S* p = new S{ 1, {2,3} }; // Creates dangling reference

--- end example] [ Note: This may introduce a dangling reference, and implementations are encouraged to issue a warning in such a case. --- end note] The destruction of a temporary whose lifetime is not extended by being bound to a reference is sequenced before the destruction of every temporary which is constructed earlier in the same full-expression. ...

In 12.3.1 [class.conv.ctor] paragraph 1, change

A constructor declared without the function-specifier explicit that can be called with a single parameter specifies a conversion from the type of its first parametertypes of its parameters to the type of its class. Such a constructor is called a converting constructor.

In 12.6.1 [class.expl.init] paragraph 2, change

When an aggregate (whether class or array) contains members of class type and is initialized by a brace-enclosed initializer-list (8.5.1), each such member is copy-initialized (see 8.5) by the corresponding assignment-expression. If there are fewer initializers in the initializer-list than members of the aggregate, each member not explicitly initialized shall be value-initialized (8.5). [ Note: 8.5.1 describes how assignment-expressions in an initializer-list are paired with the aggregate members they initialize. --end note ] An object of class type can also be initialized by a braced-init-list. List-initialization semantics apply; see 8.5 [dcl.init] and [dcl.init.list]. [ Example: ...

In 12.6.2 [class.base.init] paragraph 3, change

The expression-list or braced-init-list in a mem-initializer is used to initialize the base class or non-static data member subobject denoted by the mem-initializer-id according to the initialization rules of 8.5 [dcl.init] for direct-initialization. The semantics of a mem-initializer are as follows:

In 13.3.1p6, change

Because other than in list-initialization only one user-defined conversion is allowed in an implicit conversion sequence, special rules apply when selecting the best user-defined conversion (13.3.3,

Add a new section Initialization by list-initialization [over.match.list]

When objects of non-aggregate class type are list-initialized ([dcl.init.list]), overload resolution selects the constructor as follows:

If T has an initializer-list constructor ([dcl.init.list]), the argument list consists of the initializer list as a single argument; otherwise, the argument list consists of the elements of the initializer list.

For direct-list-initialization, the candidate functions are all the constructors of the class of the object being initialized.

For copy-list-initialization, the candidate functions are all the constructors of that class. However, if an explicit constructor is chosen, the initialization is ill-formed. [Note: This restriction only applies if this initialization is part of the final result of overload resolution --end note]

In, change

However, when considering the argument of a user-defined conversion function that is a candidate by when invoked for the copying of the temporary in the second step of a class copy-initialization, by [over.match.list] when passing the initializer list as a single argument or when the initializer list has exactly one element and a conversion to some class X or reference to (possibly cv-qualified) X is considered for the first parameter of a constructor of X, or by,, or in all cases, only standard conversion sequences and ellipsis conversion sequences are allowed.

Add a new section under []: List-initialization sequence [over.ics.list]

When an argument is an initializer list ([dcl.init.list]), it is not an expression and special rules apply for converting it to a parameter type.

If the parameter type is std::initializer_list<X> and all the elements of the initializer list can be implicitly converted to X, the implicit conversion sequence is the worst conversion necessary to convert an element of the list to X. This conversion can be a user-defined conversion even in the context of a call to an initializer-list constructor. [ Example:

  void f(std::initializer_list<int>);
  f( {1,2,3} ); // ok: f(initializer_list<int>) identity conversion
  f( {'a','b'} ); // ok: f(initializer_list<int>) integral promotion
  f( {1.0} ); // error: narrowing

  struct A {
    A(std::initializer_list<double>); // #1
    A(std::initializer_list<complex<double>>); // #2
    A(std::initializer_list<std::string>); // #3
  A a{ 1.0,2.0 }; // ok, uses #1

  void g(A);
  f({ "foo", "bar" }); // ok, uses #3

--- end example]

Otherwise, if the parameter is a non-aggregate class X and overload resolution per [over.match.list] chooses a single best constructor of X to perform the initialization of an object of type X from the argument initializer list, the implicit conversion sequence is a user-defined conversion sequence. If multiple constructors are viable but none is better than the others, the implicit conversion sequence is the ambiguous conversion sequence. User-defined conversions are allowed for conversion of the initializer list elements to the constructor parameter types except as noted in [ Example:

  struct A {
  void f(A);
  f( {'a', 'b'} ); // ok: f(A(std::initializer_list<int>)) user-defined conversion

  struct B {
    A(int, double);
  void g(B);
  g( {'a', 'b'} ); // ok: g(B(int,double)) user-defined conversion
  g( {1.0, 1,0} ); // error: narrowing

  void f(B);
  f( {'a', 'b'} ); // error: ambiguous f(A) or f(B)

  struct C {
  void h(C);
  h({"foo"}); // ok: h(C(std::string("foo")))

  struct D {
    C(A, C);
  void i(D);
  i({ {1,2}, {"bar"} }); // ok: i(D(A(std::initializer_list<int>{1,2}),C(std::string("bar"))))

--- end example]

Otherwise, if the parameter has an aggregate type which can be initialized from the initializer list according to the rules for aggregate initialization (8.5.1 [dcl.init.aggr]), the implicit conversion sequence is a user-defined conversion sequence. [ Example:

  struct A {
    int m1;
    double m2;

  void f(A);
  f( {'a', 'b'} ); // ok: f(A(int,double)) user-defined conversion 
  f( {1.0} ); // error: narrowing

--- end example]

Otherwise, if the parameter is a reference, see [over.ics.ref]. [ Note: The rules in this section will apply for initializing the underlying temporary for the reference. --end note ] [ Example:

  struct A {
    int m1;
    double m2;

 void f(const A&);
 f( {'a', 'b'} ); // ok: f(A(int,double)) user-defined conversion 
 f( {1.0} ); // error: narrowing

 void g(const double &);
 g({1}); // same conversion as int to double

--- end example]

Otherwise, if the parameter type is not a class:

In all cases other than those enumerated above, no conversion is possible.

In 14.5.3 [temp.variadic], paragraph 4, change the first bullet:

In [], paragraph 1:

Template argument deduction is done by comparing each function template parameter type (call it P) with the type of the corresponding argument of the call (call it A) as described below. If removing references and cv-qualifiers from P gives std::initializer_list<P'> for some P' and the argument is an initializer list ([dcl.init.list]), then deduction is performed instead for each element of the initializer list, taking P' as a function template parameter type and the initializer element as its argument. Otherwise, an initializer list argument causes the parameter to be considered a non-deduced context ( [Example:

  template<class T> void f(std::initializer_list<T>); // #1
  f({1,2,3}); // T deduced to int
  f({1,"asdf"}); // error: T deduced to both int and const char*

  template<class T> void g(T);
  g({1,2,3}); // error: no argument deduced for T

--- end example]

For a function parameter pack, ...

In [temp.deduct.type] paragraph 5, add as a final bullet at the top level (not the second bullet level)

In 18 [] paragraph 2, change

The following subclauses describe common type definitions used throughout the library, characteristics of the predefined types, functions supporting start and termination of a C++ program, support for dynamic memory management, support for dynamic type identification, support for exception processing, support for initializer lists, and other runtime support, as summarized in Table 16.

...and add 18.7 Initializer lists <initializer_list> to Table 16.

Add a new section after 18.7 [support.exception] and before 18.8 [support.runtime]:

18.8. Initializer lists [support.initlist]

The header <initializer_list> defines one type.

template<class E> class initializer_list {

    size_t size() const; // number of elements
    const E* begin() const; // first element
    const E* end() const; // one past the last element

An initializer_list provides access to an array of objects of type const E. [ Note: A pair of pointers or a pointer plus a length would be obvious representations for initializer_list; initializer_list is used to implement initializer lists as specified in [dcl.init.list]. Copying an initializer list does not copy the underlying elements. --- end note]

18.8.1 Initializer list constructors [support.initlist.cons]


Effects: constructs an empty initializer list
Postconditions: size() == 0
Throws: nothing

18.8.2 Initializer list access [support.initlist.access]

const E* begin() const;

Returns: a pointer to the beginning of the array; if size()==0 the value of begin() and end() are unspecified but identical; that is, begin()==end().
Throws: nothing

const E* end() const;

Returns: begin() + size()
Throws: nothing

size_t size() const;

Returns: the number of elements in the array
Throws: nothing

In 20.2 Utility components [utility] paragraph 1:

This subclause contains some basic function and class templates that are used throughout the rest of the library.

Header <utility> synopsis

      namespace std {