Containers
Consider this very typical piece of C/C++ code for creating an array of integers from a list of specified values (called a braced initializer):
const int[] someInts = {23, 10, 5, 17};
This is simple and works well where a plain int
array is needed, but sometimes we want to do something similar with STL containers. Prior to C++11, you have to go through a lot more work to create a const std::vector
with a set of pre-defined values:
std::vector<int> intArray()
{
std::vector<int> someInts;
someInts.push_back(23);
someInts.push_back(10);
someInts.push_back(5);
someInts.push_back(17);
return someInts;
}
const std::vector<int> someInts(intArray());
Because we want a const
vector, we had to build up the array in a separate function so that we could pass it to the definition of someInts
. That’s a lot of work for something that could be done on a single line for a plain int[]
array! Even if we were happy to make someInts
non-const
, it would still take quite a few lines to define the array. But with C++11, we can do much better:
const std::vector<int> someInts = {23, 10, 5, 17};
This wasn’t allowed with C++03, but with C++11 it is now valid syntax and it does what you’d intuitively expect, creating a std::vector
containing the four specified int
values. Under the covers, C++11 achieves the above using a new feature called initializer lists. Whenever a braced initializer list is encountered, the compiler converts it to an object of type std::initializer_list. The STL containers acquired additional constructors in C++11 which take a std::initializer_list
as a single parameter, allowing expressions like the above to work as expected. A few key facts about std::initializer_list
are worth understanding:
- They are always
const
objects. - Copying them does not copy their underlying data.
- They provide three member functions:
size()
,begin()
andend()
. - The
std::begin()
andstd::end()
functions have overloads forstd::initializer_list
.
The begin()
and end()
functions return const
pointers, which are essentially iterators. Hence, std::initializer_list
acts like read-only light-weight proxy for a container, so the STL and our own code are able to easily work with them in a seamless way. In a previous article, I briefly showed how initializer lists can be used in the same way as containers with ranged for loops:
for (auto item : {23, 10, 5, 17} ) { ... }
In this case, we didn’t even need to copy or assign the list of values into a specific type of container before we iterated over them. They will simply be treated as a flat array of values.
Before we move on, we should stop to consider the lifetime of the things we put in an initializer list. The values used to construct a std::initializer_list
are only guaranteed to exist for as long as the std::initializer_list
itself. Exactly how the compiler stores these objects is an implementation detail, but as long as we don’t try to access the objects of a std::initializer_list
after it is gone (which is generally also true of any container), the objects will still exist. In the case of range-based for loops, a std::initializer_list
used as the range will be guaranteed to exist until execution leaves the for loop (i.e. iteration over all objects completes or a break
statement terminates it early).
List Initialization In General
In addition to std::initializer_list
, C++11 also extends how initialization of other quantities can be expressed. The broader application of braced initializers is referred to as list initialization. One example of the improvements added is that a parenthesized parameter list can now be replaced with a braced initialization list directly in a number of situations. This includes defining objects which have a constructor taking a single std::initializer_list
parameter (e.g. all STL containers), initialization of member variables in a constructor and dynamically allocating arrays of objects. For example:
// Direct initialization of containers
std::vector<int> foo{23, 10, 5, 17};
// Initialising member variables
class MyClass
{
std::vector<int> values;
public:
MyClass() :
values{23, 10, 5, 17}
{
}
MyClass(std::initializer_list<int> seq) :
values(seq)
{
}
//...
};
// These both create the same
// equivalent MyClass object
MyClass a;
MyClass b{23, 10, 5, 17};
// Dynamic allocation with operator new[]
int* v = new int{23, 10, 5, 17};
A slightly more complicated example demonstrates how list initialization supports nesting:
std::map<char,int> charCounts
{
{'a', 3},
{'b', 11},
{'c', 8}
};
In this case, the map is initialised with a std::initializer_list
where each object in that list is defined using a list initializer as well. This is somewhat similar to how struct objects could be defined even before C++11:
struct FooBar
{
char foo;
int bar;
};
FooBar foobars[] =
{
{'a', 3},
{'b', 11},
{'c', 8},
};
Summary
The syntax improvements provided by C++11 allow the initialization of objects to be performed more concisely and intuitively. This article has discussed some of the more commonly useful aspects of initialization lists, but for a more complete picture of the different ways initialization lists can be used, this reference may be helpful.
My next article shows further uses for initialization lists when handling iteration of enum
values.
The ‘=’ is optional or “not significant” in Stroustrup’s language i.e.
const std::vector someInts = {23, 10, 5, 17};
is equivalent to
const std::vector someInts {23, 10, 5, 17};
Omitting the ‘=’ saves one character which is a small benefit to readability and it also appears to be more common to omit the ‘=’ in Stroustrup’s examples in his “C++11 FAQ” and “Uniform initialization design choices” document.