Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

std::initializer_list variations

What are the differences between the following three initializations with std::initializer_lists?

std::vector<int> a{ 2, 3, 5, 7};
std::vector<int> b( { 2, 3, 5, 7} );
std::vector<int> c = { 2, 3, 5, 7};

In the above example, std::vector is just a placeholder, but I am interested in a general answer.

like image 768
fredoverflow Avatar asked Nov 19 '12 17:11

fredoverflow


Video Answer


4 Answers

Let's abstract away from std::vector. And call it T.

T t{a, b, c};
T t = { a, b, c };
T t({a, b, c});

The first two forms are list initialization (and the only difference between them is that if T is a class, for the second explicit constructors are forbidden to be called. If one is called, the program becomes ill-formed). The last form is just ordinary direct initialization as we know it from C++03:

T t(arg);

That there appears a {a, b, c} as arg means that the argument for the constructor call is a brace initializer list. This third form does not have the special handling that list initialization has. T must be a class type there, even if the braced init list has only 1 argument. I'm glad that we put clear rules before releasing C++11 in this case.


As in terms of what constructors are called for the third, let's assume

struct T {
  T(int);
  T(std::initializer_list<int>);
};

T t({1});

Since a direct initialization is just a call to the overloaded constructors, we can transform this to

void ctor(int); 
void ctor(std::initializer_list<int>);
void ctor(T const&);
void ctor(T &&);

We can use both trailing functions, but we would need a user defined conversion if we picked these functions. To initialize the T ref parameter, list initialization will be used because this is not a direct initialization with parens (so the parameter initialization is equivalent to T ref t = { 1 }). The first two functions are exact matches. However, the Standard says that in such a case, when one function converts to std::initializer_list<T> and the other does not, then the former function wins. Therefor in this scenario, the second ctor would be used. Note that in this scenario, we will not do two-phase overload resolution with first only initializer list ctors - only list initialization will do that.


For the first two, we will use list-initialization, and it will do context dependent things. If T is an array, it will initialize an array. Take this example for a class

struct T {
  T(long);
  T(std::initializer_list<int>);
};

T t = { 1L };

In this case, we do two-phase overload resolution. We first only consider initializer list constructors and see if one matches, as argument we take the whole braced init list. The second ctor matches, so we pick it. We will ignore the first constructor. If we have no initializer list ctor or if none matches, we take all ctors and the elements of the initializer list

struct T {
  T(long);

  template<typename A = std::initializer_list<int>>
  T(A);
};

T t = { 1L };

In this case we pick the first constructor, because 1L cannot be converted to std::initializer_list<int>.

like image 128
Johannes Schaub - litb Avatar answered Oct 19 '22 10:10

Johannes Schaub - litb


In the above example, std::vector is just a placeholder, I am interested in a general answer.

How "general" of an answer do you want? Because what that means really depends on what the type you're initializing is and what constructors they have.

For example:

T a{ 2, 3, 5, 7};
T b( { 2, 3, 5, 7} );

These may be two different things. Or they may not. It depends on what constructors T has. If T has a constructor that takes a single initializer_list<int> (or some other initializer_list<U>, where U is an integral type), then both of these will call that constructor.

However, if it doesn't have that, then these two will do different things. The first, will attempt to call a constructor that takes 4 arguments that can be generated by integer literals. The second will attempt to call a constructor that takes one argument, which it will try to initialize with {2, 3, 5, 7}. This means that it will go through each one-argument constructor, figure out what the type for that argument is, and attempt to construct it with R{2, 3, 5, 7} If none of those work, then it will attempt to pass it as an initializer_list<int>. And if that doesn't work, then it fails.

initializer_list constructors always have priority.

Note that the initializer_list constructors are only in play because {2, 3, 5, 7} is a braced-init-list where every element has the same type. If you had {2, 3, 5.3, 7.9}, then it wouldn't check initializer_list constructors.

T c = { 2, 3, 5, 7};

This will behave like a, save for what kinds of conversions it will do. Since this is copy-list-initialization, it will attempt to call an initializer_list constructor. If no such constructor is available, it will attempt to call a 4-argument constructor, but it will only allow implicit conversions of its for arguments into the type parameters.

That's the only difference. It doesn't require copy/move constructors or anything (the specification only mentions copy-list-initialization in 3 places. None of them forbid it when copy/move construction is unavailable). It is almost exactly equivalent to a except for the kind of conversion it allows on its arguments.

This is why it's commonly called "uniform initialization": because it works almost the same way everywhere.

like image 40
Nicol Bolas Avatar answered Oct 19 '22 10:10

Nicol Bolas


Traditionally (C++98/03), initialization like T x(T()); invoked direct initialization, and initialization like T x = T(); invoked copy initialization. When you used copy initialization, the copy ctor was required to be present available, even though it might not (i.e., usually wasn't) used.

Initializer lists kind of change that. Looking at §8.5/14 and §8.5/15 shows that the terms direct-initialization and copy-initialization still apply -- but looking at §8.5/16, we find that for a braced init list, this is a distinction without a difference, at least for your first and third examples:

— If the initializer is a (non-parenthesized) braced-init-list, the object or reference is list-initialized (8.5.4).

As such, the actual initialization for your first and third examples is done identically, and neither requires a copy ctor (or move ctor). In both cases, we're dealing with the fourth bullet in §8.5.4/3:

— Otherwise, if T is a class type, constructors are considered. The applicable constructors are enumerated and the best one is chosen through overload resolution (13.3, 13.3.1.7). If a narrowing conversion (see below) is required to convert any of the arguments, the program is ill-formed.

... so both use std::vector's ctor that takes an std::initializer_list<T> as its argument.

As noted in the quote above, however, that only deals with a "(non-parenthesized) braced-init-list". For your second example with a parenthesized braced-init-list, we get to the first sub-bullet of the sixth bullet (geeze -- really need to talk to somebody about adding numbers for those) of §8.5/16:

— 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 (13.3.1.3), and the best one is chosen through overload resolution (13.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, or the overload resolution is ambiguous, the initialization is ill-formed.

Since this uses the syntax for direct initialization, and the expression inside the parentheses is a braced-initializer-list, and std::vector has a ctor that takes an initializer list, that's the overload that's selected.

Bottom line: although the routes through the standard to get there are different, all three end up using std::vector's constructor overload for std::initializer_list<T>. From any practical viewpoint, there's no difference between the three. All three will invoke vector::vector(std::initializer_list<T>, with no copies or other conversions happening (not even ones that are likely to be elided and really happen only in theory).

I believe with slightly different values, however, there is (or at least may be) one minor difference. The prohibition against narrowing conversions is in §8.5.4/3, so your second example (which doesn't go through §8.5.4/3, so to speak) should probably allow narrowing conversions, where the other two clearly do not. Even if I were an inveterate gambler, however, I wouldn't bet a thing on a compiler actually recognizing this distinction and allowing the narrowing conversion in the one case but not the others (I find it a little surprising, and rather doubt that it's intended to be allowed).

like image 29
Jerry Coffin Avatar answered Oct 19 '22 09:10

Jerry Coffin


I played a bit on gcc 4.7.2 with a custom class taking std::initializer_list in a constructor. I tried all those scenarios and more. It seems there is really no difference in observable results on that compiler for those 3 statements.

EDIT: This is exact code I used for testing:

#include <iostream>
#include <initializer_list>

class A {
public:
  A()                    { std::cout << "A::ctr\n"; }
  A(const A&)            { std::cout << "A::ctr_copy\n"; }
  A(A&&)                 { std::cout << "A::ctr_move\n"; }
  A &operator=(const A&) { std::cout << "A::=_copy\n"; return *this; }
  A &operator=(A&&)      { std::cout << "A::=_move\n"; return *this; }
  ~A()                   { std::cout << "A::dstr\n"; }
};

class B {
  B(const B&)            { std::cout << "B::ctr_copy\n"; }
  B(B&&)                 { std::cout << "B::ctr_move\n"; }
  B &operator=(const B&) { std::cout << "B::=copy\n"; return *this; }
  B &operator=(B&&)      { std::cout << "B::=move\n"; return *this; }
public:
  B(std::initializer_list<A> init) { std::cout << "B::ctr_ user\n"; }
  ~B()                             { std::cout << "B::dstr\n"; }
};

int main()
{
  B a1{ {}, {}, {} };
  B a2({ {}, {}, {} });
  B a3 = { {}, {}, {} };
  // B a4 = B{ {}, {}, {} }; // does not compile on gcc 4.7.2, gcc 4.8 and clang (top version)
  std::cout << "--------------------\n";
}

a1, a2 and a3 compiles fine on gcc 4.7.2, gcc 4.8 and the latest clang. I also do not see any observable results between the number of operations done on list members for all 3 cases. The last case (not from question) does not compile if I make B copy/move constructor private/deleted.

like image 1
Mateusz Pusz Avatar answered Oct 19 '22 08:10

Mateusz Pusz