18.2 Initialization
Consider our vector as it was at the end of Chapter 17:
class vector { int sz; // the size double* elem; // a pointer to the elements public: vector(int s) // constructor :sz{s}, elem{new double[s]} { /* . . . */ } // allocates memory ~vector() // destructor { delete[] elem; } // deallocates memory // . . . };
That’s fine, but what if we want to initialize a vector to a set of values that are not defaults? For example:
vector v1 = {1.2, 7.89, 12.34 };
We can do that, and it is much better than initializing to default values and then assigning the values we really want:
vector v2(2); // tedious and error-prone v2[0] = 1.2; v2[1] = 7.89; v2[2] = 12.34;
Compared to v1, the “initialization” of v2 is tedious and error-prone (we deliberately got the number of elements wrong in that code fragment). Using push_back() can save us from mentioning the size:
vector v3; // tedious and repetitive v2.push_back(1.2); v2.push_back(7.89); v2.push_back(12.34);
But this is still repetitive, so how do we write a constructor that accepts an initializer list as its argument? A { }-delimited list of elements of type T is presented to the programmer as an object of the standard library type initializer_list<T>, a list of Ts, so we can write
class vector { int sz; // the size double* elem; // a pointer to the elements public: vector(int s) // constructor (s is the element count) :sz{s}, elem{new double[sz]} // uninitialized memory for elements { for (int i = 0; i<sz; ++i) elem[i] = 0.0; // initialize } vector(initializer_list<double> lst) // initializer-list constructor :sz{lst.size()}, elem{new double[sz]} // uninitialized memory // for elements { copy( lst.begin(),lst.end(),elem); // initialize (using std::copy(); §B.5.2) } // . . . };
We used the standard library copy algorithm (§B.5.2). It copies a sequence of elements specified by its first two arguments (here, the beginning and the end of the initializer_list) to a sequence of elements starting with its third argument (here, the vector’s elements starting at elem).
Now we can write
vector v1 = {1,2,3}; // three elements 1.0, 2.0, 3.0 vector v2(3); // three elements each with the (default) value 0.0
Note how we use ( ) for an element count and { } for element lists. We need a notation to distinguish them. For example:
vector v1 {3}; // one element with the value 3.0 vector v2(3); // three elements each with the (default) value 0.0
This is not very elegant, but it is effective. If there is a choice, the compiler will interpret a value in a { } list as an element value and pass it to the initializer-list constructor as an element of an initializer_list.
In most cases — including all cases we will encounter in this book — the = before an { } initializer list is optional, so we can write
vector v11 = {1,2,3}; // three elements 1.0, 2.0, 3.0 vector v12 {1,2,3}; // three elements 1.0, 2.0, 3.0
The difference is purely one of style.
Note that we pass initializer_list<double> by value. That was deliberate and required by the language rules: an initializer_list is simply a handle to elements allocated “elsewhere” (see §B.6.4).