10.3. Container classes

Container classes are simply user defined types that provide easy access to a collection of data that is the same type. You are already familiar with several container classes:

There are more in the STL, but they all have one thing in common. They store a collection of data of a single type.

Containers are like arrays in that regard, however, container classes have many features that raw arrays lack.

C++11 adds a list initialization as a feature. List initialization provides 2 key benefits:

  1. No implicit narrowing conversions

  2. No floating point to integral type conversions

  3. Ability to initialize a container using a list syntax

Consider the following examples.

double d = 3.14;
int pi = d;      // compiles, but probably a bug

int  i  {d};     // compile error
char c1 {i};     // compile error
char c2 {128};   // ok - 128 fits in type char
int  x  {c2};    // ok - widening conversion

double z {x};    // compile error
                 // yes, a widening conversion, but
                 // no floating point to int conversions allowed
                 // in either direction

What about ‘initializer list’ syntax?

An initializer list is a type constructed when using list initialization followed by an assignment operator.

auto a = 1;       // a is an int
auto b {2};       // b is an int
auto c = {3};     // c is an initializer_list<int>

The type of c can be unexpected when the type is not a container type. It is important to note the initializer_list constructor is preferred above all others if it exists in a class.

10.3.1. Initializer list constructors

The initializer_list was introduced in C++11 to simplify container initialization and make it more uniform. Prior to C++11, you could initialize an array with a value list:

int fib[] = {1, 1, 2, 3, 5, 8, 13};

However, trying the same style for other standard library containers would result in a compile error:

// compile error in C++03 and prior
std::vector<int> fib = {1, 1, 2, 3, 5, 8, 13};

Prior to C++11, if you wanted to add a known set of values to a vector you would have to push them back one at a time using push_back, either one line at a time, in a loop, or using an algorithm from the STL.

An initializer_list<T> is a lightweight wrapper that that creates a temporary array of type T.

std::initializer_list<int> fib = {1, 1, 2, 3, 5, 8, 13};

Any class can define a constructor that takes a initializer_list type as a parameter. Obviously, it makes sense to define an initializer_list constructor only for a type that can be correctly constructed from a list of values. Containers are the most obvious example and are the motivation behind initializer lists.

The following example defines the absolute minimum you would need to define a simple class that can be initialized like std::array.

It defines a stack array of specified size using the non-type parameter N and allows a new array to be constructed using (only) an initializer list. No other constructors are included in this example.

template <class T, size_t N>
struct array {
    T data_[N];
    array(const std::initializer_list<T>& list) {
      std::copy(list.begin(), list.end(), data_);
    }
};

An array instance can now be created from an initializer list using standard C++ syntax.

array<int,7> fib = {1,1,2,3,5,8,13};

The assignment from the temporary list to the array object does require an implicit conversion between the two types.

This example converts the struct into a class and makes the one argument constructor explicit.

Notice how the declaration of variable fib changes in main when the initializer_list constructor is marked as explicit.

Note: Initializer lists will always favor a matching initializer_list constructor over other potentially matching constructors. So, if our array had a one argument constructor that took a single value of type T:

array<int,1> fib = {3};

Then this declaration would invoke array(const std::initializer_list<T>&) and not array(int). If you want to match to int constructor once a list constructor has been defined, you’ll need to use copy initialization or direct initialization. The rule applies to std::vector and other standard library container classes that define both a list constructor and another one argument conversion constructor.

// Calls std::vector::vector(std::size_type)
// creates 3 value-initialized elements: 0 0 0
std::vector<int> data( 3 );

// Calls std::vector::vector(std::initializer_list<int>)
// creates 1 element with value = 3
std::vector<int> data{ 3 };

You have attempted of activities on this page