What are the differences between C-like, constructor, and uniform initialization?
What are the differences between c-like, constructor, and uniform initialization?
For primitive types like int
, there's no practical difference; so let's consider a class type T
instead.
The first style is equivalent to
T x(T(0));
creating a temporary object from the initialiser expression, and then initialising x
by moving or copying that. In practice, the move or copy will be elided, so that the result is the same as the second style; the only difference is that the first will fail if there isn't an accessible copy or move constructor.
The second directly initialises the object using a constructor that takes one argument, giving an error if there's no suitable constructor.
The third depends on what constructors are available.
- if there's a constructor taking
std::initializer_list
, it uses that; - otherwise, if there's a constructor taking a single argument of a suitable type, it uses that;
- otherwise, if it's an aggregate (with no constructors) with one member, that member is initialised with zero;
- otherwise, it's an error.
And should I always use the uniform initialization?
No. Sometimes you need function-style initialisation to distinguish between an initializer_list
constructor and one taking other argument types. For example:
std::vector<int> v1(10, 42); // 10 elements with value 42
std::vector<int> v2{10, 42}; // 2 elements with values 10 and 42
You also shouldn't call it "uniform initialisation" since it's not "uniform" in any meaningful sense. The official term is "brace-initialisation".
First, I would recommend looking at the following talk by Herb Sutter, in which he gives some recommendations about the subject. The brace-initialization discussion starts at around 23:00.
When you are talking about primitive data types, all 3 yield the same result. I personally prefer sticking with the old int x = 0
syntax, but it comes down to personal preference.
For class types, brace initialization and old-school constructor initialization are not completely interchangeable. For example:
vector<int> v (100); // Creates a 100-element vector
vector<int> v {100}; // Creates a 1-element vector, holding the value 100.
This is because std::vector
has a constructor that explicitly defines std::initializer_list
as its only argument. Keep in mind that
auto var = {1, 2};
creates a std::initializer_list
, with var
as its identifier.
The thing about initializer lists is that they provide consistency that is a welcome change from what was available beforehand. For example, if you were to initialize an array in C++, you would use:
int arr[] = {1, 2, 3, 4};
But, if you wanted to initialize a vector<int>
with the same elements, you either had to:
- Initialize the above arr first and then pass
arr
andarr + 4
- Create the vector and push_back() the elements individually or in a loop.
With C++11, you could just use
vector<int> v = {1, 2, 3, 4}; // Same syntax. Nice! Note that the = is optional
Another instance in which brace initialization is helpful is that it provides a workaround to C++'s most vexing parse. From the talk, assume that we have two classes, origin
and extents
, whose instances can be passed to construct another object of type rectangle
. The following statement:
rectangle w(origin(), extents());
doesn't allow you to create a rectangle
object using origin
and extents
temporaries, because that statement is parsed as a function declaration. Tsk tsk. So normally, you would have to do:
origin o;
extents e;
rectangle w(o, e);
With brace initialization, you can create them on the fly, and
rectangle w {origin(), extents()};
will work as intended, i.e. passed to the constructor which is overloaded with an origin
object as it's first argument and an extents
object as the second.
The rule is for objects, use brace initialiation unless you have a reason not to.