Differences between direct-list-initialization and copy-list-initialization

I vastly prefer copy-list initialization in general (and particularly in the case of std::vector) because direct-list initialization is very similar at a glance to std::vector's actual constructors when there is only one or two elements.

std::vector<int> x(2);
// is very different from
std::vector<int> x{2};

Being explicit about the fact that I'm assigning an initial value to the vector and not configuring it with the values is less prone to misreadings.


List initialization is informally called "uniform initialization" because its meaning and behavior is intended to be the same regardless of how you invoke it.

Of course, C++ being C++, what is "intended" doesn't always happen.

There are basically three major differences between the behavior of direct-list-initialization and copy-list-initialization. The first is the one you will encounter most frequently: if list initialization would call a constructor marked explicit, then there is a compile error if the list-initialization form is copy-list-initialization.

This behavioral difference is defined in [over.match.list]/1:

In copy-list-initialization, if an explicit constructor is chosen, the initialization is ill-formed.

That's a function of overload resolution.

The second major difference (new to C++17) is that, given an enum-type with a fixed underlying size, you can perform direct-list-initialization on it with a value of the underlying type. But you cannot perform copy-list-initialization from such a value. So enumeration e{value}; works, but not enumeration e = {value};.

The third major difference (also new to C++17) relates to the behavior of braced-init-lists in auto deduction. Normally, auto behaves not too distinctly from template argument deduction. But unlike template argument deduction, auto can be initialized from a braced-init-list.

If you initialize an auto variable using direct-list-initialization with a single expression in the list, the compiler will deduce the variable to be of the type of the expression:

auto x{50.0f}; //x is a `float`.

Sounds reasonable. But if do the exact same thing with copy-list-initialization, it will always be deduced to an initializer_list<T>, where T is the type of the initializer:

auto x = {50.0f}; //x is an `initializer_list<float>`

So very, very uniform. ;)

Thankfully, if you use multiple initializers in the braced-init-list, direct-list-initialization for an auto-deduced variable will always give a compile error, while copy-list-initialization will just give a longer initializer_list. So auto-deduced direct-list-initialization will never give an initializer_list, while auto-deduced copy-list-initialization always will.

There are some minor differences that rarely affects the expected behavior of the initialization. These are cases where list-initialization from a single value will use copy or direct (non-list) initialization as appropriate to the list-initialization form. These cases are:

  1. Initializing an aggregate from a single value which is the same type as the aggregate being initialized. This bypasses aggregate initialization.

  2. Initializing a non-class, non-enumeration type from a single value.

  3. Initializing a reference.

Not only do these not happen particularly frequently, they basically never really change the meaning of the code. Non-class types don't have explicit constructors, so the difference between copy and direct initialization is mostly academic. Same goes for references. And the aggregate case is really just about performing a copy/move from a given value.