Skip to content

Modern C++ • Uniform Initialization

Proper initialization of data is a fundamental concern in programming, especially when the environment does not provide a consistent behavior, which can lead to unexpected results. Prior to C++11, initialization could be performed in a variety of ways and you had to be very careful of what you were doing in order not to leave the objects in an undefined, potentially dangerous state. Since C++11 things have improved significantly and developers have now more consistent and intuitive initialization constructs that simplify old clunky approaches and address some issues that were long overdue.

Before diving into the boring code, let’s revise some language and terminology that will help us put things in the right perspective. There are a variety of ways in which data objects can be initialized, depending on their type and the context they’re in, and in the course of its existence C++ has added new methods and modified the behaviour of existing ones. At the time of this writing, C++ supports the following initialization types:

  • value initialization: refers to the initialization of an object when it is constructed by invoking an initializer expression (i.e. a constructor) without any arguments
  • direct initialization: refers to the initialization of an object when it is constructed by invoking an initializer expression (i.e. a constructor) with an explicit set of arguments
  • copy initialitazion: refers to the initialization of an object when it is constructed copying the values from another object (i.e. in assignments, pass-by-value, return-by-value)
  • default initialization: refers to the initialization of an object constructed without invoking an inizializer expression (i.e. when a constructor is implicitly invoked)
  • zero initialization: refers to the initialization of an object with a “zero” value
  • list initialization (C++11): refers to the initialization of an object using an initializer list {...} (braced-init-list)
  • aggregate initialization (C++11): refers to the initialization of aggregate objects using an initializer list (a special case of list initialization)

The initialization process may involve several of the above mentioned initialization types and each one has a range of effects on the data being initialized depending on the type, the context and the C++ version. These specifications have changed in the course of the C++ modernization process to accommodate the implementation of new features, so you are invited to read the the appropriate C++ Standard drafts for all the esoteric details about specific versions.

When it comes to C++ initialization we are generally speaking of the following data types

  • Primitive (integer, floating point, boolean, character, etc.)
  • Aggregates (broadly speaking, a set of publicly accessible data values, such as arrays and PODs)
  • Members (data types defined in a class)
  • Containers (data structures such as vectors, lists, maps, etc.)

These objects can be initialized in different ways in pre C++11, and for each type there could be several initialization methods, as illustrated in the following code


// Primitives initialization

int x = 5;           // copy-initialization
int z(3);            // direct-initialization
int t();             // ERROR: ambiguous (function declaration ?)
int w = int();       // value-initialization then copy-initialization
double d = 1.3;
double pi(3.14);
char c = 'A';
int* p = &x;
int* p2(NULL);

// Aggregates initialization

int age[3] = {28, 35, 43};            // copy-list-initialization
char hello[] = "ciao";
char greet[] = {'c','i','a','o','\0'};

int* years = new int[3];              // can't be initialized
     years[0] = 1980;                 // individual elements must be
     years[1] = 1975;                 // assigned after construction
     years[2] = 1985;

struct M {
   int x; int y;
};

M m1 = {1,2};                         // copy-list-initialization
M m2;                                 // uninitialized
M *m3 = new M;                        // uninitialized

struct C { 
   C() : x(0), y(0) {}
   C(int a, int b) : x(a), y(b) {}
   int x; int y;
}
C c1;                                 // default-initialization
C c2(1,2)                             // direct initialization
C c3{4,5}                             // direct-list-initialization

// Members initialization

class A
{
    int x = 1;   // ERROR: not allowed for non-static non-const members
    int ages[3];
    M m;
public:
    A(int x) : 
      x(x), 
      ages(???),    // can't initialize arrays here
      m(???)        // can't initialize PODs here
    {
        ages[0] = 25;
        ages[1] = 35;
        ages[2] = 45;

        m.x = -1;
        m.y = 10;
    }
};

// Containers initialization

std::vector<int> ages;         // can't be initialized

ages.push_back(28);            // individual elements must be
ages.push_back(35);            // added after construction
ages.push_back(43);

std::map<std::string, int> people;  // same as above ...

people["Alex"] = 28;
people["Justin"] = 35;
people["Mark"] = 43;

class B
{
    std::vector<int> ages;
    std::map<std::string, int> people;

    B() : 
       ages(???), people(???)  // can't initialize containers here
    {
         // Containers must be initialized here
         // by adding individual elements
    }
};

As can be seen from the examples above, pre C++11 had some very annoying limitations. Specifically, 1) dynamically allocated arrays and PODs could not be initialized; 2) arrays and PODs could not be initialized in member initialization lists; 3) containers could not be initialized; 4) non-static non-const class members could not be initialized at the class level. Furthermore, the initialization methods were not consistent as there were many ways to perform objects initialization with possibly different effects.

The C++11 standard introduced some new features:

1. An extended initializer list by introduction of initializer_list objects;
2. The ability to initialize any class member at the class level; and
3. A unified syntax for a more intuitive and consistent initialization;

 

Initializer Lists

The braced initializer list {...} has been extended to containers as well so that they can be easily initialized with a sequence of elements at construction time, like automatically allocated arrays and PODs

std::vector<int> years = { 1975, 1980, 2018 };   // (copy) list-initialization

std::vector<std::string> names {                 // (direct) list-initialization
	"Alberto", "John", "Mark"
};

std::list<std::string> places {
	"Rome", "Melbourne", "Tokyo"
};

std::map<std::string, int> people {
    {"Albert", 614092837 },
    {"John", 390398273},
    {"Mark", 619283330}
};

class A 
{
	std::vector<int> ages;
	std::list<std::string> places;
	std::map<std::string, int> people;
public:
	A() :  // containers can now be initialized in member init lists
		ages{ 25, 35, 45 },
		places{ "Rome", "Melbourne", "Tokyo" },
		people{ 
                    { "Albert", 614092837 },
	            { "John", 390398273 },
	            { "Mark", 619283330 } }
	{
	}
};

This is possible thanks to an auxiliary object of type initializer_list<T> that’s automatically constructed by the compiler from initializer lists. What happens under the hood is this: 1) the elements in the initializer list are assigned to a temporary array of type const T[]; 2) an initializer_list<T> object is constructed to reference the elements of the array (multiple copies of initializer_list<T> are possible and they will all reference the same array); 3) the initializer_list<T> object is passed to the container into a specific constructor called the initializer-list constructor (or sequence constructor); 4) the container uses the initializer list object to access the elements in the initializer list and initialize its own elements. The initializer_list object is basically a const iterator with a size() method that allows read-only access to the elements of the initializer list that it wraps.

But the initializer_list object is not only limited for use by the compiler with container classes. Being a first-class citizen with a type of its own, it can be used as any other object and passed as a parameter or returned as a value, as shown in the following code

class MyCollection
{
	std::list<int> collection;
public:
        // Initializer-list constructor
	MyCollection(std::initializer_list<int> items)
	{
		collection.insert(collection.end(), items);
	}

        // A method taking an initializer_list as an argument
	void add(std::initializer_list<int> items)
	{
		collection.insert(collection.end(), items);
	}

	void add(int item)
	{
		collection.push_back(item);
	}
};

MyCollection collection { 1,2,3 }; // calls the initializer-list constructor
collection.add({ 4,5,6 });
collection.add(7);

With this mechanism it is possible to conveniently pass list of elements into custom classes or functions allowing for a more seamless coding experience.

 

Class Member Initialization

Along with the standard member initializer list, C++11 provides another mechanism to initialize class members to default values directly at the class level, that is at the point where they’re declared, as illustrated in the following code

struct P {
	int x; int y;
};

class A 
{
	double pi    = 3.14;
	char letter  = 'A';
	int months   = 12;
	int *ptr     = nullptr;
	int years[3] = { 1975, 1980, 1985 };  //can also be initialized in the member initializer list
	P point      = { 1,-3 };

	std::vector<int> ages = { 25, 35, 45 };
	std::list<std::string> places = { "Rome", "Melbourne", "Tokyo" };
	std::map<std::string, int> people = { { "Albert", 614092837 },
	                                      { "John", 390398273 },
	                                      { "Mark", 619283330 } };
};

Class member initialization can also be done through the member initializer list, but doing it at the class level provides more flexibility as it allows you to specify a class-level default value that can then be overridden in the member initializer list (which takes precedence) if needed. Also, it removes clutter as you don’t have to repeat it in every constructor if there are many of them, thus making the code more readable.

 

Uniform Initialization

C++11 extends the concept of initializer list even further to unify the way data types are initialized and provide a consistent syntax. In fact, aside from aggregates and classes, initialization lists can also be used with primitive types

int a{ 5 };       // = 5
int b = { 3 };    // = 3
int c{};          // = 0
char d{ 'A' };    // = 'A'
char e{};         // = ''
double f{ 0.5 };  // = 0.5
double g{ f };    // = 0.5

and with dynamically allocated objects

int *a = new int[5] { 1,2,3 };                         // [1,2,3,0,0]
char *dev = new char[7] { 'A','L','B','E','R','T' };   // ['A','L','B','E','R','T',\0]
double *debts= new double[6]{};                        // [0,0,0,0,0,0]
const char **fruits = new const char*[3] {             // ["Apple","Kiwi","Mango"]
	"Apple", "Kiwi", "Mango"
};

struct P { int x; int y; };

P *point = new P{-30, 28};

The idea is that of using a single notation for all kinds on initialization that will make the code more readable, maintainable, concise and intuitive, as shown in the below example

// Old C++

int a = 5;
int b(5);
int c();  // ERROR: ambiguous
int d[5] = {1,2,3,4,5};
int *e = new int[3];
     e[0] = 1; e[1] = 2; e[2] = 3;

class C
{
     int a[3];
     double rate;
     std::vector<int> ages;
public:
     C() : rate(2.5)
     {
          a[0] = 1;
          a[1] = 2;
          a[2] = 3;

          ages.push_back(25);
          ages.push_back(30);
          ages.push_back(35);
     }
};

// Modern C++

int a = {5};
int b {5};
int c {};
int d[5] {1,2,3,4,5};
int *e = new int[3] {1,2,3};

class C
{
     int a[3]              {1,2,3};
     double rate           {2.5};
     std::vector<int> ages {25,30,35};
};

One benefit of using initializer lists is that they do not allow narrowing. Narrowing is the (side) effect that arises when a value is assigned to another value with a smaller (narrower) type. For example, assigning a floating point value to an integer. Historically, narrowing has been quite a controversial topic and, while it is a potential source of bugs, it has become an established practice for doing implicit conversions. Initializer lists forbid this kind of operation, as illustrated in the following code

int i1 = 543;
float f1 = 1.5f;
double d1 = 2.1;

// Narrowed values
char c1 = i1;            // c1 = 31
int i2 = f1;             // i2 = 1
float f2 = d1;           // f2 = 2.09999
	
char c2 { i1 };          // ERROR: requires a narrowing conversion int->char
int i3  { f1 };          // ERROR: requires a narrowing conversion float->int
float f3 { d1 };         // ERROR: requires a narrowing conversion double->float
char c3 { 260 };         // ERROR: requires a narrowing conversion int->char
char c4 { 10 };          // OK: 10 is an int but fits into a char
double d2 { 10 };        // OK: int fits into a double
int i4 { 5.6 };          // ERROR: requires a narrowing conversion double->int
unsigned int i5 { -1 };  // ERROR: requires a narrowing conversion int->unsigned int
vector<int> v {1,2.5,3}  // ERROR: 2.5 requires a narrowing conversion double->int

All in all, it appears that, apart from edge cases, there are many reasons to prefer the new uniform initialization over the old inconsistent method of initializing objects. The use of the braced-init-list notation {...} provides for a more consistent programming experience.

Published inModern C++