Brace-initialization is a uniform method for initializing data in C++11. For this reason, it is also called uniform initialization. It is arguably one of the most important features from C++11 that developers should understand and use. It removes previous distinctions between initializing fundamental types, aggregate and non-aggregate types, and arrays and standard containers.
Understanding uniform initialization
Getting ready
For continuing with this recipe, you need to be familiar with direct initialization that initializes an object from an explicit set of constructor arguments and copy initialization that initializes an object from another object. The following is a simple example of both types of initialization, but for further details, you should see additional resources:
std::string s1("test"); // direct initialization
std::string s2 = "test"; // copy initialization
How to do it...
To uniformly initialize objects regardless of their type, use the brace-initialization form {} that can be used for both direct initialization and copy initialization. When used with brace initialization, these are called direct list and copy list initialization.
T object {other}; // direct list initialization
T object = {other}; // copy list initialization
Examples of uniform initialization are as follows:
- Standard containers:
std::vector<int> v { 1, 2, 3 };
std::map<int, std::string> m { {1, "one"}, { 2, "two" }};
- Dynamically allocated arrays:
int* arr2 = new int[3]{ 1, 2, 3 };
- Arrays:
int arr1[3] { 1, 2, 3 };
- Built-in types:
int i { 42 };
double d { 1.2 };
- User-defined types:
class foo
{
int a_;
double b_;
public:
foo():a_(0), b_(0) {}
foo(int a, double b = 0.0):a_(a), b_(b) {}
};
foo f1{};
foo f2{ 42, 1.2 };
foo f3{ 42 };
- User-defined POD types:
struct bar { int a_; double b_;};
bar b{ 42, 1.2 };
How it works...
Before C++11 objects required different types of initialization based on their type:
- Fundamental types could be initialized using assignment:
int a = 42;
double b = 1.2;
- Class objects could also be initialized using assignment from a single value if they had a conversion constructor (prior to C++11, a constructor with a single parameter was called a conversion constructor):
class foo
{
int a_;
public:
foo(int a):a_(a) {}
};
foo f1 = 42;
- Non-aggregate classes could be initialized with parentheses (the functional form) when arguments were provided and only without any parentheses when default initialization was performed (call to the default constructor). In the next example, foo is the structure defined in the How to do it... section:
foo f1; // default initialization
foo f2(42, 1.2);
foo f3(42);
foo f4(); // function declaration
- Aggregate and POD types could be initialized with brace-initialization. In the next example, bar is the structure defined in the How to do it... section:
bar b = {42, 1.2};
int a[] = {1, 2, 3, 4, 5};
Apart from the different methods of initializing the data, there are also some limitations. For instance, the only way to initialize a standard container was to first declare an object and then insert elements into it; vector was an exception because it is possible to assign values from an array that can be prior initialized using aggregate initialization. On the other hand, however, dynamically allocated aggregates could not be initialized directly.
All the examples in the How to do it... section use direct initialization, but copy initialization is also possible with brace-initialization. The two forms, direct and copy initialization, may be equivalent in most cases, but copy initialization is less permissive because it does not consider explicit constructors in its implicit conversion sequence that must produce an object directly from the initializer, whereas direct initialization expects an implicit conversion from the initializer to an argument of the constructor. Dynamically allocated arrays can only be initialized using direct initialization.
Of the classes shown in the preceding examples, foo is the one class that has both a default constructor and a constructor with parameters. To use the default constructor to perform default initialization, we need to use empty braces, that is, {}. To use the constructor with parameters, we need to provide the values for all the arguments in braces {}. Unlike non-aggregate types where default initialization means invoking the default constructor, for aggregate types, default initialization means initializing with zeros.
Initialization of standard containers, such as the vector and the map also shown above, is possible because all standard containers have an additional constructor in C++11 that takes an argument of type std::initializer_list<T>. This is basically a lightweight proxy over an array of elements of type T const. These constructors then initialize the internal data from the values in the initializer list.
The way the initialization using std::initializer_list works is the following:
- The compiler resolves the types of the elements in the initialization list (all elements must have the same type).
- The compiler creates an array with the elements in the initializer list.
- The compiler creates an std::initializer_list<T> object to wrap the previously created array.
- The std::initializer_list<T> object is passed as an argument to the constructor.
An initializer list always takes precedence over other constructors where brace-initialization is used. If such a constructor exists for a class, it will be called when brace-initialization is performed:
class foo
{
int a_;
int b_;
public:
foo() :a_(0), b_(0) {}
foo(int a, int b = 0) :a_(a), b_(b) {}
foo(std::initializer_list<int> l) {}
};
foo f{ 1, 2 }; // calls constructor with initializer_list<int>
The precedence rule applies to any function, not just constructors. In the following example, two overloads of the same function exist. Calling the function with an initializer list resolves to a call to the overload with an std::initializer_list:
void func(int const a, int const b, int const c)
{
std::cout << a << b << c << std::endl;
}
void func(std::initializer_list<int> const l)
{
for (auto const & e : l)
std::cout << e << std::endl;
}
func({ 1,2,3 }); // calls second overload
This, however, has the potential of leading to bugs. Let's take, for example, the vector type. Among the constructors of the vector, there is one that has a single argument representing the initial number of elements to be allocated and another one that has an std::initializer_list as an argument. If the intention is to create a vector with a preallocated size, using the brace-initialization will not work, as the constructor with the std::initializer_list will be the best overload to be called:
std::vector<int> v {5};
The preceding code does not create a vector with five elements, but a vector with one element with a value 5. To be able to actually create a vector with five elements, initialization with the parentheses form must be used:
std::vector<int> v (5);
Another thing to note is that brace-initialization does not allow narrowing conversion. According to the C++ standard (refer to paragraph 8.5.4 of the standard), a narrowing conversion is an implicit conversion:
- From long double to double or float, or from double to float, except where the source is a constant expression and the actual value after conversion is within the range of values that can be represented (even if it cannot be represented exactly)
- From an integer type or unscoped enumeration type to a floating-point type, except where the source is a constant expression and the actual value after conversion will fit into the target type and will produce the original value when converted to its original type
- From an integer type or unscoped enumeration type to an integer type that cannot represent all the values of the original type, except where the source is a constant expression and the actual value after conversion will fit into the target type and will produce the original value when converted to its original type.
The following declarations trigger compiler errors because they require a narrowing conversion:
int i{ 1.2 }; // error
double d = 47 / 13;
float f1{ d }; // error
float f2{47/13}; // OK
To fix the error, an explicit conversion must be done:
int i{ static_cast<int>(1.2) };
double d = 47 / 13;
float f1{ static_cast<float>(d) };
There's more
The following sample shows several examples of direct-list-initialization and copy-list-initialization. In C++11, the deduced type of all these expressions is std::initializer_list<int>.
auto a = {42}; // std::initializer_list<int>
auto b {42}; // std::initializer_list<int>
auto c = {4, 2}; // std::initializer_list<int>
auto d {4, 2}; // std::initializer_list<int>
C++17 has changed the rules for list initialization, differentiating between the direct- and copy-list-initialization. The new rules for type deduction are as follows:
- for copy list initialization auto deduction will deduce a std::initializer_list<T> if all elements in the list have the same type, or be ill-formed.
- for direct list initialization auto deduction will deduce a T if the list has a single element, or be ill-formed if there is more than one element.
Base on the new rules, the previous examples would change as follows: a and c are deduced as std::initializer_list<int>; b is deduced as an int; d, which uses direct initialization and has more than one value in the brace-init-list, triggers a compiler error.
auto a = {42}; // std::initializer_list<int>
auto b {42}; // int
auto c = {4, 2}; // std::initializer_list<int>
auto d {4, 2}; // error, too many
See also
- Using auto whenever possible
- Understanding the various forms of non-static member initialization