Why you should prefer the uniform initialization in modern C++
Tags: cpp
Published: 2023-02-13 17:31:10


C++ offers various syntax choices for object initialization. Initialization values may be specified with parenthesis, an equal signs or braces. In many cases, it is also possible to use an equals sign and braces together which usually gets treated as same as the braces-only version. For eg., an int can be initlized in the following ways. ([Yes, an int is an object](https://stackoverflow.com/questions/48026922/is-an-int-variable-an-object-according-to-the-c-standard))

```
int i1(0);                  // initializer in paranthesis
int i2 = 0;                 // initializer follows "="
int i3{0} or int i3 = {0};  // initializer in braces.

```

There are several reasons to prefer braced initialization or “Uniform Initialization”  over other initializing expressions. Here is a look at some of them.

## 1. Initialize aggregate and POD types

Both standard and dynamically allocated containers as well as POD types can be initialized with default values which were formerly not possible.
```
std::vector<int> v1{ 1, 2, 3 };
int *a1 = new int[3] { 5, 6, 7 };

struct Bar
{
   int m_Value1;
   int m_Value2;
};
Bar bar { 1, 7.6 };
```
## 2. Assign default values to non-static data members of a class
This capability is shared with the “=” initializer syntax but not with paranthesis.
```
class Foo
{
   …
private:
   int x{ 0 }; // fine, x's default value is 0
   int y = 0;  // also fine
   int z(0);   // error!
};
```
## 3. Prohibits implicit narrowing conversions
Braced initialization prohibits implicit narrowing conversions among built-in types. An error is thrown for non expressible values inside the braced initialization.

```
int i1 = 5.1;  // warning: possible loss of data
int i2(5.2);   // warning: possible loss of data
int i3{ 5.3 }; // error: requires a narrowing conversion
```
## 4. Avoid Most Vexing Parse
The Most Vexing Parse is when C++ cannot distinguish between the creation of an object and declaration of a function. This is because anything that can be parsed as a declaration must be interpreted as one.

```
class Foo
{
public:
   Foo()      { /* do something */ }
   Foo(int i) { /* do something */ }
};

int main()
{
   Foo foo1(15); // call Foo ctor with arg 15
   Foo foo2();   // declares a function named foo2 that returns a Foo object

   return EXIT_SUCCESS;
}
```
Braced initialization prevents this since functions cannot be declared using braces.

```
Foo foo2{}; // calls ctor with no args

```
## Using braced initialization with the std::initializer_list
So are there no drawbacks to braced initialization ? Is it the preferred style for all cases ?

The answer is an unfortunate ‘No’. One drawback to braced initialization is sometimes the surprising behavior that results from a tangled relationship among braced initializers, std::initializer_list and constructor overloading resolution. Consider the following class
```
class Foo
{
public:
   Foo(int f_value1, double f_value2) : m_Value1{ f_value1 },
                                        m_Value2{ f_value2 } {}

   Foo(std::initializer_list<int> f_list) : m_Value1{std::accumulate(
                                                        f_list.begin(),
                                                        f_list.end(),
                                                        0) },
                                            m_Value2{ 0.0 } {}
private:
   int    m_Value1;
   double m_Value2;
};
```
During constructor call, the constructor with the std::initializer_list takes precedence over the other constructor overloads.
```
Foo foo1{ 15, 32, 27 }; // calls ctor with initalizer list
Foo foo2{ 85, 45 };     // also calls ctor with initalizer list
```
In case of foo2, we would expect the constructor Foo(int f_value1, double f_value2) to be called, but the constructor with the initializer list takes precedence during constructor overload resolution. Thus adding a constructor with an initializer list might break legacy code that uses braced initialization for constructor calls.


The fix ? Introduce an empty struct/class as an argument in the constructor with the initializer list to force constructor overloading resolution to choose other versions of the constructor.
```
struct list_picker { };

class Foo
{
public:
   Foo(int f_value1, int f_value2) : m_Value1{ f_value1 },
                                     m_Value2{ f_value2 } {}

   Foo(list_picker,
       std::initializer_list<int> f_list) : m_Value1{ std::accumulate(
                                                        f_list.begin(), 
                                                        f_list.end(),
                                                        0) },
                                            m_Value2{ 0.0 } {}

private:
   int m_Value1;
   int m_Value2;
};
```
Now with the above modified constructor, the constructor overloading resolution happens as follows
```
Foo foo1{ 15, 32, 27 }; // calls ctor with initalizer_list
Foo foo2{ 85, 45 };     // also calls ctor with int and double as args
```
If you need to call the constructor with the initializer_list, you can do the following
```
Foo foo3{ {}, { 12, 45} };
```