What Is a Curly-Brace Enclosed List If Not an Intializer_List

What Is a Curly-Brace Enclosed List If Not an intializer_list?

There are three distinct, but related concepts here:

  1. braced-init-list: The grammatical rule associated with curly-brace-enclosed lists in certain contexts.

  2. Initializer list: The name for the braced-init-list initializer used in list-initialization.

  3. std::initializer_list: A class wrapping a temporary array which is created in some contexts involving braced-init-lists.

Some examples:

//a braced-init-list and initializer list, 
//but doesn't create a std::initializer_list
int a {4};

//a braced-init-list and initializer list,
//creates a std::initializer_list
std::vector b {1, 2, 3};

//a braced-init-list and initializer list,
//does not create a std::initializer_list (aggregate initialization)
int c[] = {1, 2, 3};

//d is a std::initializer_list created from an initializer list
std::initializer_list d {1, 2, 3};

//e is std::initializer_list<int>
auto e = { 4 };

//f used to be a std::initializer_list<int>, but is now int after N3922
auto f { 4 };

You might want to read N3922, which changed some of the rules involving auto and std::initializer_list.

Curly braces constructor prefers initializer_list over better match. Why?

Because initializer_list constructors, if at all possible, take precedence over other constructors. This is to make edge cases less confusing - specifically, this particular vector constructor that you expect it to use was deemed too easily selected by accident.

Specifically, the standard says in 16.3.1.7 "Initialization by list-initialization" [over.match.list] (latest draft, N4687):

(1) When objects of non-aggregate class type T are list-initialized such that 11.6.4 specifies that overload resolution is performed according to the rules in this section, overload resolution selects the constructor in two phases:

  • Initially, the candidate functions are the initializer-list constructors (11.6.4) of the class T and the argument list consists of the initializer list as a single argument.
  • If no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all the constructors of the class T and the argument list consists of the elements of the initializer list.

So if you do std::vector<char>( i, c ), this section does not apply at all, since it isn't list-initialization. Normal overload resolution is applied, the (size_t, char) constructor is found, and used.

But if you do std::vector<char>{ i, c }, this is list-initialization. The initializer list constructors are tried first, and the (initializer_list<char>) constructor is a match (even though it involves the narrowing conversion from size_t to char), so it is used before the size+value constructor is ever considered.

So to answer the edited-in question: no, you can't create the vector without naming its type. But in C++17 you can use class template argument deduction and simply write return std::vector(i, c);

When to use the brace-enclosed initializer?

I think the following could be a good guideline:

  • If the (single) value you are initializing with is intended to be the exact value of the object, use copy (=) initialization (because then in case of error, you'll never accidentally invoke an explicit constructor, which generally interprets the provided value differently). In places where copy initialization is not available, see if brace initialization has the correct semantics, and if so, use that; otherwise use parenthesis initialization (if that is also not available, you're out of luck anyway).

  • If the values you are initializing with are a list of values to be stored in the object (like the elements of a vector/array, or real/imaginary part of a complex number), use curly braces initialization if available.

  • If the values you are initializing with are not values to be stored, but describe the intended value/state of the object, use parentheses. Examples are the size argument of a vector or the file name argument of an fstream.

Is This Actually Ambiguous?

The rule is in [over.ics.list]:

Otherwise, 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, or if the initializer list has no elements, the identity conversion.

In both your overloads, the worst conversion necessary is the identity conversion. So we have two implicit conversion sequences with rank identity. There is a rule in [over.match.best] which prefers a list-initialization of a std::initializer_list<X> over alternatives (so std::initializer_list<int> is preferred to int for {1}), but there nothing to suggest that this rule should apply recursively.

Since there's nothing to disambiguate the two conversion sequences, the call is ambiguous. gcc and clang are correct to reject.

I need the option of passing in a vector of objects, or brace-enclosed list, to a constructor

On that third line of code it works with replacing initializer_list with vector, true. But I don't want to pass around a whole vector as an arg.

You're going to construct a vector anyway (One::athree_). So just move the vector passed to the constructor rather than copy it:

class One: public Base {
public:
One(Two* ptwo_in, std::vector<Three> ilthree)
: ptwo{ptwo_in}
, athree_{std::move(ilthree)}
{ }

private:
Two* ptwo_;
std::vector<Three> athree_;
};

This is a common pattern in C++. Pass by non-const value and use move semantics to avoid copies:

One one{some_ptr, {Three{args}, Three{args}, Three{args}}};

or:

std::vector<Three> vec{ ... };

// moves 'vec' to the ctor parameter, the ctor then moves it to its member
One one{some_ptr, std::move(vec)};

No unnecessary copies that way.

Why doesn't C++11 curly brace initialzation in constructor initialization list work when parens initializaton does?

Short answer: This was a bug in the Standard which is fixed in C++14, and g++ 4.9 has the fix (retroactively applied to C++11 mode too). Defect Report 1288


Here's a simpler example:

struct S
{
int x;
S() { } // this causes S to not be an aggregate (otherwise aggregate
// initialization is used instead of list initialization)
};

S x = 5;
S const &y { x } ;

x = 6;
std::cout << y << std::endl; // output : 5

In the text of C++11, the meaning of S const &y {x}; is not to bind y to x; in fact the meaning is to create a temporary and bind a reference to that. From C++11 [dcl.init.ref]/3:

Otherwise, if T is a reference type, a prvalue 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. —end note ]

This is pretty silly , clearly the intent of this code is to bind y directly to x. In C++14 the text was changed:

Otherwise, if the initializer list has a single element of type E and either T is not a reference type or its referenced type is reference-related to E, the object or reference is initialized from that element;

Since a type is reference-related to itself (or one of its base classes), in my sample here and in your actual code, it should actually bind correctly.


Your error message comes from the compiler following the C++11 wording and attempting to create a temporary from base to bind the reference to; and this fails because base is of an abstract type.

Why does GCC 6.3 compile this Braced-Init-List code without explicit C++11 support?

The default compiler command for gcc 6.x is -std=gnu++14, so the compiler is implicitly compiling your code using a later version of the C++ language standard.

You will need to manually specify -std=c++03 if you want to compile in C++03.



Related Topics



Leave a reply



Submit