Chapter 10. The Standard Template Library

What you will learn in this Chapter:

  • The capabilities offered by the STL

  • How to create and use containers

  • How to use iterators with containers

  • The types of algorithms that are available with the STL, and how you can apply the more common ones

  • How to use function objects with the STL

  • How to define lambda expressions and use them with the STL

  • How to use polymorphic function wrappers with lambda expressions

  • How to use the STL version that supports C++/CLI class types

At its name implies, the Standard Template Library (STL) is a library of standard class and function templates. You can use these templates to create a wide range of powerful general-purpose classes for organizing your data, as well as functions for processing that data in various ways. The STL is defined by the standard for native C++ and is therefore always available with a conforming compiler. Because of its broad applicability, the STL can greatly simplify programming in many of your C++ applications.

Of course, the STL for native C++ does not work with C++/CLI class types, but in Visual C++ 2010 you have an additional version of the STL available that contains templates and generic functions that you can instantiate with C++/CLI class types.

WHAT IS THE STANDARD TEMPLATE LIBRARY?

The STL is a large collection of class and function templates that is provided with your native C++ compiler. I'll first explain, in general terms, the kinds of resources the STL provides and how they interact with one another, before diving into the details of working examples. The STL contains six kinds of components: containers, container adapters, iterators, algorithms, function objects, and function adapters. Because they are part of the standard library, the names of the STL components are all defined within the std namespace.

The STL is a very large library, some of which is highly specialized, and to cover the contents fully would require a book in its own right. In this chapter, I'll introduce the fundamentals of how you use the STL and describe the more commonly used capabilities. Before getting into containers in depth, I'll introduce you to the primary components, concepts, and terminology you will find in the STL.

Containers

Containers are objects that you use to store and organize other objects. A class that implements a linked list is an example of a container. You create a container class from an STL template by supplying the type of the object that you intend to store. For example, vector<T> is a template for a container that is a linear array that automatically increases in size when necessary. T is the type parameter that specifies the type of objects to be stored. Here are a couple of statements that are examples of creating vector<T> containers:

vector<string> strings;     // Stores object of type string
vector<double> data;        // Stores values of type double

I chose the vector container as an example because it is probably used most often. The first statement creates the container class, strings, that stores objects of type string, while the second statement creates the data container that stores values of type double.

You can store items of a fundamental type, or of any class type, in a container. If your type argument for an STL container template is a class type, the container can store objects of that type, or objects of any derived class type. Typically, containers store copies of the objects that you store in them, and they automatically allocate and manage the memory that the objects occupy. When a container object is destroyed, the container takes care of deleting the objects it contains and freeing the memory they occupied. One advantage of using STL containers to store your objects is that it relieves you of the chore of managing the memory for them.

The templates for the STL container classes are defined in the standard headers shown in the following table.

HEADER FILE

CONTENTS

vector

A vector<T> container represents an array that can increase in capacity automatically when required. You can only add new elements efficiently to the end of a vector container. Adding elements to the middle of a vector can involve a lot of overhead.

deque

A deque<T> container implements a double-ended queue. This is equivalent to a vector but with the additional capability for you to efficiently add elements to the beginning.

list

A list<T> container is a doubly-linked list.

map

A map<K, T> is an associative container that stores each object (of type T) with an associated key (of type K) that determines where the key/object pair is located. The value of each key in a map must be unique.

This header also defines the multimap<K,T> container where the keys in the key/object pairs do not need to be unique.

set

A set<T> container is a map where each object serves as its own key. All objects in a set must be unique. A consequence of using an object as its own key is that you cannot change an object in a set; to change an object you must delete it and then insert the modified version.

This header also defines the multiset<T> container, which is like a set container except that the entries do not need to be unique.

bitset

Defines the bitset<T> class template that represents a fixed number of bits. This is typically used to store flags that represent a set of states or conditions.

The containers in this table represent the complete set that is available with the STL. All the template names are defined within the std namespace. T is the template type parameter for the type of elements stored in a container; where keys are used, K is the type of key.

Microsoft Visual C++ also includes the headers hash_map and hash_set that define templates for the hash_map<K, T> and hash_set<K, T> containers. These are variations on the map<K, T> and set<K, T> containers. The standard map and set containers use an ordering mechanism to locate entries whereas the hash_map and hash_set containers use a hashing mechanism.

Container Adapters

The STL also defines container adapters. A container adapter is a template class that wraps an existing STL container class to provide a different, and typically more restricted, capability. The container adapters are defined in the headers in the following table.

HEADER FILE

CONTENTS

queue

A queue<T> container is defined by an adapter from a deque<T> container by default, but you could define it using a list<T> container. You can only access the first and last elements in a queue, and you can only add elements at the back and remove them from the front. Thus, a queue<T> container works more or less like the queue in your local coffee shop.

This header also defines a priority_queue<T> container, which is a queue that orders the elements it contains so that the largest element is always at the front. Only the element at the front can be accessed or removed. A priority queue is defined by an adapter from a vector<T> by default, but you could use a deque<T> as the base container.

stack

A stack<T> container is defined by an adapter from a deque<T> container by default, but you could define it using a vector<T> or a list<T> container. A stack is a last-in first-out container, so adding or removing elements always occurs at the top, and you can only access the top element.

Iterators

Iterators are objects that behave like pointers and are very important for accessing the contents of all STL containers, except for those defined by a container adapter; container adapters do not support iterators. You can obtain an iterator from a container, which you can use to access the objects you have previously stored. You can also create iterators that will allow input and output of objects, or data items of a given type, from, or to, a native C++ stream. Although basically all iterators behave like pointers, not all iterators provide the same functionality. However, they do share a base level of capability. Given two iterators, iter1 and iter2, accessing the same set of objects, the comparison operations iter1 == iter2, iter1 != iter2, and the assignment iter1 = iter2 are always possible, regardless of the types of iter1 and iter2.

There are four different categories of iterators; each category supports a different range of operations, as shown in the following table. The operations described for each category are in addition to the three operations that I mentioned in the previous paragraph.

ITERATOR CATEGORY

DESCRIPTION

Input and output iterators

These iterators read or write a sequence of objects and may only be used once. To read or write a second time, you must obtain a new iterator. You can perform the following operations on these iterators:

++iter or iter++
*iter

For the dereferencing operation, only read access is allowed in the case of an input iterator, and only write access for an output iterator.

Forward iterators

Forward iterators incorporate the capabilities of both input and output iterators, so you can apply the operations shown above to them, and you can use them for access and store operations. Forward iterators can also be reused to traverse a set of objects in a forward direction as many times as you want.

Bidirectional iterators

Bidirectional iterators provide the same capabilities as forward iterators and additionally allow the operations --iter and iter--. This means you can traverse backward as well as forward through a sequence of objects.

Random access iterators

Random access iterators have the same capabilities as bidirectional iterators but also allow the following operations:

iter+n or iter-n
iter += n or iter += n
iter1 + iter2
iter1 < iter2 or iter1 > iter2
iter1 <= iter2 or iter1 >= iter2
iter[n]

Being able to increment or decrement an iterator by an arbitrary value n allows random access to the set of objects. The last operation using the [] operator is equivalent to *(iter + n).

Thus, iterators in the four successive categories provide a progressively greater range of functionality. Where an algorithm requires an iterator with a given level of functionality, you can use any iterator that provides the required level of capability. For example, if a forward iterator is required, you must use at least a forward iterator; an input or an output operator will not do. On the other hand, you could also use a bidirectional iterator or a random access iterator because they both have the capability provided by a forward iterator.

Note that when you obtain an iterator to access the contents of a container, the kind of iterator you get will depend on the sort of container you are using. The types of some iterators can be complex, but as you'll see, in many instances the auto keyword can deduce the type for you if you initialize the iterator when you create it.

Algorithms

Algorithms are STL function templates that operate on a set of objects provided to them by an iterator. Because the objects are supplied by an iterator, an algorithm needs no knowledge of the source of the objects to be processed. The objects could be retrieved by the iterator from a container or even from a stream. Because iterators work like pointers, all STL template functions that accept an iterator as an argument will work equally well with a regular pointer.

As you'll see, you will frequently use containers, iterators, and algorithms in concert, in the manner illustrated in Figure 10-1.

FIGURE 10-1

Figure 10.1. FIGURE 10-1

When you apply an algorithm to the contents of a container, you supply iterators that point to objects within the container. The algorithm uses these iterators to access objects within the container and to write them back when appropriate. For example, when you apply the sort() algorithm to the contents of a vector, you pass two iterators to the sort() function. One points to the first object, and the other points to the position that is one past the last element in the vector. The sort() function uses these iterators to access objects for comparison, and to write the objects back to the container to establish the ordering. You see this working in an example later in this chapter.

Algorithms are defined in two standard header files, the algorithm header and the numeric header.

Function Objects in the STL

Function objects are objects of a class type that overloads the () operator (the function call operator), which means that the class implements the operator()() function. The STL defines a set of standard function objects as templates; you can use these to create a function object, where the overloaded () operator function works with your object type. For example, the STL defines the template less<T>. If you instantiate the template as less<myClass>, you have a type for function objects that implement operator()() to provide the less-than comparison for objects of type myClass.

Many algorithms make use of function objects to specify binary operations to be carried out, or to specify predicates that determine how or whether a particular operation is to be carried out. A predicate is a function that returns a value of type bool, and because a function object is an object of a type that implements the operator()() member function to return a value of type bool, a function object can also be a predicate. For example, suppose you have defined a class type Comp that implements the operator()() function to compare its two arguments and return a bool value. If you create an object f of type Comp, the expression f(a,b) returns a bool value that results from comparing a and b, and thus acts as a predicate.

Predicates come in two flavors, binary predicates that involve two operands, and unary predicates that require one operand. For example, comparisons such as less-than and equal-to, and logical operations such as AND and OR, are implemented as binary predicates that are members of function objects; logical negation, NOT, is implemented as a unary predicate member of a function object.

Function object templates are defined in the functional header; you can also define your own function objects when necessary. You'll see function objects in action with algorithms, and some container class functions, later in this chapter.

Function Adapters

Function adapters are function templates that allow function objects to be combined to produce a more complex function object. A simple example is the not1 function adapter. This takes an existing function object that provides a unary predicate and inverts it. Thus, if the function object function returns true, the function that results from applying not1 to it will be false. I won't be discussing function adapters in depth, not because they are terribly difficult to understand — they aren't — but because there's a limit to how much I can cram into a single chapter.

THE RANGE OF STL CONTAINERS

The STL provides templates for a variety of container classes that you can use in a wide range of application contexts. Sequence containers are containers in which you store objects of a given type in a linear fashion, either as a dynamic array or as a list. Associative containers store objects based on a key that you supply with each object to be stored; the key is used to locate the object within the container. In a typical application, you might be storing phone numbers in an associative container, using names as the keys. This would enable you to retrieve a particular number from the container just by supplying the appropriate name.

I'll first introduce you to sequence containers, and then I'll delve into associative containers and what you can do with them.

SEQUENCE CONTAINERS

The class templates for the three basic sequence containers are shown in the following table.

TEMPLATE

HEADER FILE

DESCRIPTION

vector<T>

vector

Creates a class representing a dynamic array storing objects of type T.

list<T>

list

Creates a class representing a linked list storing objects of type T.

deque<T>

deque

Creates a class representing a double-ended queue storing objects of type T.

Which template you choose to use in any particular instance will depend on the application. These three kinds of sequence containers are clearly differentiated by the operations they can perform efficiently, as Figure 10-2 shows.

FIGURE 10-2

Figure 10.2. FIGURE 10-2

If you need random access to the contents of the container, and you are happy to always add or delete objects at the end of a sequence, then vector<T> is the container template to choose. It is possible to add or delete objects randomly within a vector, but the process will be very slow because all the objects past the insertion or deletion point will have to be moved. A deque<T> container is very similar to a vector<T> and supports the same operations, but it has the additional capability to add and delete at the beginning of the sequence. A list<T> container is a doubly-linked list, so adding and deleting at any position is efficient. The downside of a list is that there is no random access to the contents; the only way to access an object that is internal to the list is to traverse the contents from the beginning, or to run backward through the contents from the end.

Let's look at sequence containers in more detail and try some examples. I'll be introducing the use of some iterators, algorithms, and function objects along the way.

Creating Vector Containers

The simplest way to create a vector container is like this:

vector<int> mydata;

This creates a container that will store values of type int. The initial capacity to store elements is zero, so you will be allocating more memory right from the outset when you insert the first value. The push_back() function adds a new element to the end of a vector, so to store a value in this vector you would write:

mydata.push_back(99);

The argument to the push_back() function is the item to be stored. This statement stores the value 99 in the vector, so after executing this statement the vector contains one element.

Here's another way to create a vector to store integers:

vector<int> mydata(100);

This creates a vector that contains 100 elements that are all initialized to zero. If you add new elements to this vector, the memory allocated for storage in the vector will be increased automatically, so obviously it's a good idea to choose a reasonably accurate value for the number of integers you are likely to want to store. This vector already contains 100 elements and can be used just like an array. For example, to store a value in the third element, you can write:

mydata[2] = 999;

Of course, you can only use an index value to access elements within a vector that are within the range of elements that exist. You can't add new elements in this way, though. To add a new element, you should use the push_back() function.

You can initialize the elements in a vector to a different value, when you create it by using this statement:

vector<int> mydata(100, −1);

The second argument to the constructor is the initial value to be used, so all 100 elements in the vector will be set to −1.

If you don't want to create elements when you create the container, you can increase the capacity after you create it by calling its reserve() function:

vector<int> mydata;
mydata.reserve(100);

The argument to the reserve() function is the minimum number of elements to be accommodated. If the argument is less than the current capacity of the vector, then calling reserve() will have no effect. In this code fragment, calling reserve() causes the vector container to allocate sufficient memory for a total of 100 elements.

You can also create a vector with initial values for elements from an external array. For example:

double data[] = {1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5, 10.5};
vector<double> mydata(data, data+8);

Here the data array is created with 10 elements of type double, with the initial values shown. The second statement creates a vector storing elements of type double, with eight elements initially having the values corresponding to data[0] through data[7]. The arguments to the vector<double> constructor are pointers (and can also be iterators), where the first pointer points to the first initializing element in the array, and the second points to one past the last initializing element. Thus, the mydata vector will contain eight elements with initial values 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, and 8.5.

Because the constructor in the previous fragment can accept either pointer or iterator arguments, you can initialize a vector when you create it with values from another vector that contains elements of the same type. You just supply the constructor with an iterator pointing to the first element you want to use as an initializer, plus a second iterator pointing to one past the last element you want to use. Here's an example:

vector<double> values(mydata.begin(), mydata.end());

After executing this statement, the values vector will have elements that are duplicates of the mydata vector. As Figure 10-3 illustrates, the begin() function returns a random access iterator that points to the first element in the vector for which it is called, and the end() function returns a random access iterator pointing to one past the last element. A sequence of elements is typically specified in the STL by two iterators, one pointing to the first element in the sequence and the other pointing to one past the last element in the sequence, so you'll see this time and time again.

FIGURE 10-3

Figure 10.3. FIGURE 10-3

Because the begin() and end() functions for a vector container return random access iterators, you can modify what they point to when you use them. The type of the iterators that the begin() and end() functions return is vector<T>::iterator, where T is the type of object stored in the vector.

Here's a statement that creates a vector that is initialized with the third through the seventh elements from the mydata vector:

vector<double> values(mydata.begin()+2, mydata.end()−1);

Adding 2 to the first iterator makes it point to the third element in mydata. Subtracting 1 from the second iterator makes it point to the last element in mydata; remember that the second argument to the constructor is an iterator that points to a position that is one past the element to be used as the last initializer, so the object that the second iterator points to is not included in the set.

As I said earlier, it is pretty much standard practice in the STL to indicate a sequence of elements in a container by a begin iterator that points to the first element and an end iterator that points to one past the last element. This method allows you to iterate over all the elements in the sequence by incrementing the begin iterator until it equals the end iterator. This means that the iterators only need to support the equality operator to allow you to walk through the sequence.

Occasionally, you may want to access the contents of a vector in reverse order. Calling the rbegin() function for a vector returns an iterator that points to the last element, while rend() points to one past the first element (that is, the position preceding the first element), as Figure 10-4 illustrates.

FIGURE 10-4

Figure 10.4. FIGURE 10-4

The iterators returned by rbegin() and rend() are called reverse iterators because they present the elements in reverse sequence. Reverse iterators are of type vector<T>::reverse_iterator. Figure 10-4 shows how adding a positive integer to the rbegin() iterator moves back through the sequence, and subtracting an integer from rend() moves forward through the sequence.

Here's how you could create a vector containing the contents of another vector in reverse order:

double data[] = {1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5, 10.5};
vector<double> mydata(data, data+8);
vector<double> values(mydata.rbegin(), mydata.rend());

Because you are using reverse iterators as arguments to the constructor in the last statement, the values vector will contain the elements from mydata in reverse order.

The Capacity and Size of a Vector Container

It's about time I explained the difference between the capacity and the size of a vector container. The capacity is the maximum number of objects a container can currently accommodate without allocating more memory. The size is the number of objects actually stored in the container. So, the size cannot be greater than the capacity.

You can obtain both the size and capacity of the container data at any time by calling the size() and capacity() member functions. For example:

cout << endl << "The current capacity of the container is: " << data.capacity()
     << endl << "The current size of the container is: " << data.size() << endl;

Calling the capacity() function for a vector returns the current capacity, and calling its size() function returns the current size, both values being returned as type vector<T>::size_type. This is an implementation-defined integer type that is defined within the vector<T> class template by a typedef. To create a variable to store the value returned from the size() or capacity() function, you specify it as type vector<T>::size_type, where you replace T with the type of object stored in the container. The following fragment illustrates this:

vector<double> values;
vector<double>::size_type cap = values.capacity();

Note

The Microsoft Visual C++ library implementation of STL defines the vector<T>::size_type type as size_t. size_t is an unsigned integer type that is also a type of the result of the sizeof operator. You could use the auto keyword to specify the type of cap in the fragment above.

If the value returned by the size() function is zero, then clearly the vector contains no elements; thus, you can use it as a test for an empty vector. You can also call the empty() function for a vector to test for this:

if(values.empty())
  cout << "No more elements in the vector."

The empty() function returns a value of type bool that is true when the vector is empty and false otherwise.

You are unlikely to need it very often, but you can discover the maximum possible number of elements in a vector by calling its max_size() function. For example:

vector<string> strings;
cout << "Maximum length of strings vector: " << strings.max_size();

Executing this fragment produces the output:

Maximum length of strings vector: 153391689

The maximum length is returned by the max_size() function as a value, in this case of type vector<string>::size_type. Note that the maximum length of a vector will depend on the type of element stored in the vector. If you try this out with a vector storing values of type int, you will get 1073741823 as the maximum length, and for a vector storing value of type double it is 536870911.

You can change the size of a vector by calling its resize() function, which can either increase or decrease the size of the vector. If you specify a new size that is less than the current size, sufficient elements will be deleted from the end of the vector to reduce it to its new size. If the new size is greater than the old, new elements will be added to the end of the vector to increase its length to the new size. Here's code illustrating this:

vector<int> values(5, 66);   // Contains 66 66 66 66 66
values.resize(7, 88);        // Contains 66 66 66 66 66 88 88
values.resize(10);           // Contains 66 66 66 66 66 88 88 0 0 0
values.resize(4);            // Contains 66 66 66 66

The first argument to resize() is the new size for the vector. The second argument, when it is present, is the value to be used for new elements that need to be added to make up the new size. If you are increasing the size and you don't specify a value to be used for new elements, the default value will be used. In the case of a vector storing objects of a class type, the default value will be the object produced by the no-arg constructor for the class.

You can explore the size and capacity of a vector through the following working example.

Accessing the Elements in a Vector

You have already seen that you can access the elements in a vector by using the subscript operator, just as you would for an array. You can also use the at() function where the argument is the index position of the element you want to access. Here's how you could list the contents of the numbers vector of integer elements in the previous example:

for(auto i = 0; i<numbers.size() ; i++)
    cout << " " << numbers.at(i);

So how does the at() function differ from using the subscript operator, []? Well, if you use a subscript with the subscript operator that is outside the valid range, the result is undefined. If you do the same with the at() function, an exception of type out_of_range will be thrown. If there's the potential for subscript values outside the legal range to arise in a program, it's generally better to use the at() function and catch the exception than to allow the possibility for undefined results.

To access the first or last element in a vector container you can call the front() or back() function respectively:

cout << "The value of the first element is: " << numbers.front() << endl;
cout << "The value of the last element is: " << numbers.back() << endl;

Both functions come in two versions; one returns a reference to the object stored, and the other returns a const reference to the object stored. The latter option enables you to explicitly prevent modification of the object:

const int& firstvalue = numbers.front();    // firstvalue cannot be changed
int& lastvalue = numbers.back();            // lastvalue can be changed

Storing the reference that is returned in a const variable automatically selects the version of the function that returns a const reference.

Inserting and Deleting Elements in a Vector

In addition to the push_back() function you have seen, a vector container supports the pop_back() operation that deletes the last element. Both operations execute in constant time, that is, the time to execute will be the same, regardless of the number of elements in the vector. The pop_back() function is very simple to use:

vec.pop_back();

This statement removes the last element from the vector vec and reduces the size by 1. If the vector contains no elements, then calling pop_back() has no effect.

You could remove all the elements in a vector by calling the pop_back() function repeatedly, but the clear() function does this much more simply:

vec.clear();

This statement removes all the elements from vec, so the size will be zero. Of course, the capacity will be left unchanged.

You can call the insert() function to insert one or more new elements anywhere in a vector, but this operation will execute in linear time, which means that the time will increase in proportion to the number of elements in the container. This is because inserting new elements involves moving the existing elements. The simplest version of the insert() function inserts a single new element at a specific position in the vector, where the first argument is an iterator specifying the position where the element is to be inserted, and the second argument is the element to be inserted. For example:

vector<int> vec(5, 99);
vec.insert(vec.begin()+1, 88);

The first statement creates a vector with five integer elements, all initialized to 99. The second statement inserts 88 after the first element; so, after executing this, the vector will contain:

99 88 99 99 99 99

You can also insert several identical elements, starting from a given position:

vec.insert(vec.begin()+2, 3, 77);

The first argument is an iterator specifying the position where the first element is to be inserted, the second argument is the number of elements to be inserted, and the third argument is the element to be inserted. After executing this statement, vec will contain:

99 88 77 77 77 99 99 99 99

You have yet another version of the insert() function that inserts a sequence of elements at a given position. The first argument is an iterator pointing to the position where the first element is to be inserted. The second and third arguments are input iterators specifying the range of elements to be inserted from some source. Here's an example:

vector<int> newvec(5, 22);
newvec.insert(newvec.begin()+1, vec.begin()+1, vec.begin()+5);

The first statement creates a vector with five integer elements initialized to 22. The second statement inserts four elements from vec, starting with the second. After executing these statements, newvec will contain:

22 88 77 77 77 22 22 22 22

Don't forget that the second iterator in the interval specifies the position that is one past the last element, so the element it points to is not included.

The erase() function can delete one or more elements from any position within a vector, but this also is a linear time function and will typically be slow. Here's how you erase a single element at a given position:

newvec.erase(newvec.end()−2);

The argument is an iterator that points to the element to be erased, so this statement removes the second to last element from newvec.

To delete several elements, you supply two iterator arguments specifying the interval. For example:

newvec.erase(newvec.begin()+1, newvec.begin()+4);

This will delete the second, third, and fourth elements from newvec. The element that the second iterator argument points to is not included in the operation.

As I said, both the erase() and insert() operations are slow, so you should use them sparingly when working with a vector. If you find you need to use them often in your application, a list<T> is likely to be a better choice of container.

The swap() function enables you to swap the contents of two vectors, provided, of course, the elements in the two vectors are of the same type. Here's a code fragment showing an example of how this works:

vector<int> first(5, 77);              // Contains 77 77 77 77 77
vector<int> second(8, −1);             // Contains −1 −1 −1 −1 −1 −1 −1
first.swap(second);

After executing the last statement, the contents of the vectors first and second will have interchanged. Note that the capacities of the vectors are swapped as well as the contents, and, of course, the size.

The assign() function enables you to replace the entire contents of a vector with another sequence, or to replace the contents with a given number of instances of an object. Here's how you could replace the contents of one vector with a sequence from another:

vector<double> values;
for(int i = 1 ; i <= 50 ; i++)
  values.push_back(2.5*i);
vector<double> newdata(5, 3.5);
newdata.assign(values.begin()+1, values.end()−1);

This code fragment creates the values vector and stores 50 elements that have the values 2.5, 5.0, 7.5,... 125.0. The newdata vector is created with five elements, each having the value 3.5. The last statement calls the assign() function for newdata, which deletes all elements from newdata, and then inserts copies of all the elements from values, except for the first and the last. You specify the new sequence to be inserted by two iterators, the first pointing to the first element to be inserted and the second pointing to one past the last element to be inserted. Because you specify the new elements to be inserted by two iterators, the source of the data can be from any sequence, not just a vector. The assign() function will also work with regular pointers, so you could also insert elements from an array of double elements.

Here's how you use the assign() function to replace the contents of a vector with a sequence of instances of the same element:

newdata.assign(30, 99.5);

The first argument is the count of elements in the replacement sequence, and the second argument is the element to be used. This statement will cause the contents of newdata to be deleted and replaced by 30 elements, each having the value 99.5.

Storing Class Objects in a Vector

So far, you have only seen vectors storing numerical values. You can store objects of any class type in a vector, but the class must meet certain minimum criteria. Here's a minimum specification for a given class T to be compatible with a vector, or, in fact, any sequence container:

class T
{
  public:
  T();                                 // Default constructor
  T(const T& t);                       // Copy constructor
  ~T();                                // Destructor
  T& operator=(const T& t);            // Assignment operator
};

Of course, the compiler will supply default versions of these class members if you don't supply them, so it's not difficult for a class to meet these requirements. The important thing to note is that they are required and are likely to be used, so when the default implementation that the compiler supplies will not suffice, you must provide your own implementation.

Note that the STL can make use of a move constructor and a move assignment operator when they are provided in a class, so the performance of your class may be increased if you implement these as well.

Let's try an example.

Sorting Vector Elements

The sort() function template that is defined in the algorithm header will sort a sequence of objects identified by two random access iterators that point to the first and one-past-the-last objects in the sequence. Note that random access iterators are essential; iterators with lesser capability will not suffice. The sort() function template uses the < operator to order the elements. Thus, you can use the sort() template to sort the contents of any container that provides random access iterators, as long as the objects it contains can be compared using the less-than operator.

In the previous example, you implemented operator<() in the Person class, so you can sort a sequence of Person objects. Here's how you could sort the contents of the vector<Person> container:

sort(people.begin(), people.end());

This sorts the contents of the vector in ascending sequence. You can add an #include directive for algorithm, and put the statement in main() before the output loop, to see the sort in action. You'll also need a using declaration for std::sort.

Note that you can use the sort() template function to sort() arrays. The only requirement is that the < operator should work with the type of elements stored in the array. Here's a code fragment showing how you could use it to sort an array of integers:

const size_t max(100);
int data[max];
cout << "Enter up to " << max << " non-zero integers. Enter 0 to end." << endl;
int value(0);
size_t count(0);
for(size_t i = 0 ; i<max ; i++)        // Read up to max integers
{
  cin >> value;                        // Read a value
  if(value == 0)                       // If it is zero,
    break;                             // We are done
  data[count++] = value;
}
sort(data, data+count);                // Sort the integers

Note how the pointer marking the end of the sequence of elements that are to be sorted must still be one past the last element.

When you need to sort a sequence in descending order, you can use a version of the sort() algorithm that accepts a function object that is a binary predicate, as the third argument to the function. The functional header defines a complete set of types for comparison predicates:

less<T>   less_equal<T>   equal<T>   greater_equal<T>   greater<T>

Each of these templates creates a class type for function objects that you can use with sort() and other algorithms. The sort() function used in the previous fragment uses a less<int> function object by default. To specify a different function object to be used as the sort criterion, you add it as a third argument, like this:

sort(data, data+count, greater<int>());     // Sort the integers

The third argument to the function is an expression that calls the constructor for the greater<int> type, so you are passing an object of this type to the sort() function. This statement will sort the contents of the data array in descending sequence. If you are trying these fragments out, don't forget that you need the functional header to be included for the function object, and that the greater name is defined in the std namespace.

Storing Pointers in a Vector

A vector container, like other containers, makes a copy of the objects you add to it. This has tremendous advantages in most circumstances, but it could be very inconvenient in some situations. For example, if your objects are large, there could be considerable overhead in copying each object as you add it to the container. This is an occasion where you might be better off storing pointers to the objects in the container rather than the objects themselves, and managing the objects externally. You could create a new version of the Ex10_02.cpp example to store pointers to Person objects in a container.

Double-Ended Queue Containers

The double-ended queue container template, deque<T>, is defined in the deque header. A double-ended queue container is very similar to a vector in that it can do everything a vector container can, and includes the same function members, but you can also add and delete elements efficiently at the beginning of the sequence as well as at the end. You could replace the vector used in Ex10_02.cpp with a double-ended queue, and it would work just as well:

deque<Person> people;               // Double-ended queue of Person objects

Of course, you would need to change the #include directive to include the deque header instead of the vector header.

The function to add an element to the front of the container is push_front(), and you can delete the first element by calling the pop_front() function. Thus, if you were using a deque<Person> container in Ex10_02.cpp, you could add elements at the front instead of the back:

people.push_front(Person(firstname, secondname));

The only difference in using this statement to add elements to the container would be that the order of the elements in the double-ended queue would be the reverse of what they are in the vector.

The range of constructors available for a deque<T> container is the same as for vector<T>. Here are examples of each of them:

deque<string> strings;            // Create an empty container
deque<int> items(50);
 // A container of 50 elements initialized to
// default value
deque<double> values(5, 0.5);     // A container with 5 elements 0.5
deque<int> data(items.begin(), items.end());  // Initialized with a sequence

Although a double-ended queue is very similar to a vector and does everything a vector can do, as well as allowing you to add to the front of the sequence efficiently, it does have one disadvantage compared to a vector. Because of the additional capability it offers, the memory management for a double-ended queue is more complicated than for a vector, so it will be slightly slower. Unless you need the ability to add elements to the front of the container, a vector is a better choice.

Let's see a double-ended queue in action.

Using List Containers

The List<T> container template that is defined in the list header implements a doubly-linked list. The big advantage a list container has over a vector or a double-ended queue is that you can insert or delete elements anywhere in the sequence in constant time. The range of constructors for a list container is similar to that for a vector or double-ended queue. This statement creates an empty list:

list<string> names;

You can also create a list with a given number of default elements:

list<string> sayings(20);              // A list of 20 empty strings

Here's how you create a list containing a given number of elements that are identical:

list<double> values(50, 2.71828);

This creates a list of 50 values of type double.

Of course, you can also construct a list initialized with values from a sequence specified by two iterators:

list<double> samples(++values.begin(), --values.end());

This creates a list from the contents of the values list, omitting the first and last elements in values. Note that the iterators returned by the begin() and end() functions for a list are bidirectional iterators, so you do not have the same flexibility as with a vector or a deque container that supports random access iterators. You can only change the value of a bidirectional iterator using the increment or decrement operator.

Just like the other sequence containers, you can discover the number of elements in a list by calling its size() member function. You can also change the number of elements in a list by calling its resize() function. If the argument to resize() is less than the number of elements in the list, elements will be deleted from the end; if the argument is greater, elements will be added using the default constructor for the type of elements stored.

Adding Elements to a List

You add an element to the beginning or end of a list by calling push_front() or push_back(), just as you would for a double-ended queue. To add elements to the interior of a list, you use the insert() function, which comes in three versions. Using the first version, you can insert a new element at a position specified by an iterator:

list<int> data(20, 1);                 // List of 20 elements value 1
data.insert(++data.begin(), 77);       // Insert 77 as the second element

The first argument to insert() is an iterator specified in the insertion position, and the second argument is the element to be inserted. The increment operator applied to the bidirectional iterator returned by begin() makes it point to the second element in the list. After executing this, the list contents will be:

1 77 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1

You can see that the list now contains 21 elements, and that the elements from the insertion point on are simply displaced to the right.

You can also insert a number of copies of the same element at a given position:

list<int>::iterator iter = data.begin();
for(int i = 0 ; i<9 ; i++)
  ++iter;
data.insert(iter, 3, 88);   // Insert 3 copies of 88 starting at the 10th

The first argument to the insert() function is an iterator specifying the position, the second argument is the number of elements to be inserted, and the third argument is the element to be inserted repeatedly. To get to the tenth element you increment the iterator nine times in the for loop. Thus, this fragment inserts three copies of 88 into the list, starting at the tenth element. Now the contents of the list will be:

1 77 1 1 1 1 1 1 1 88 88 88 1 1 1 1 1 1 1 1 1 1 1 1

Now the list contains 24 elements.

Here's how you can insert a sequence of elements into a list:

vector<int> numbers(10, 5);            // Vector of 10 elements with value 5
data.insert(--(--data.end()), numbers.begin(), numbers.end());

The first argument to insert() is an iterator pointing to the second to last element position. The sequence to be inserted is specified by the second and third arguments to the insert() function, so this will insert all the elements from the vector into the list, starting at the second to last element position. After executing this, the contents of the list will be:

1 77 1 1 1 1 1 1 1 88 88 88 1 1 1 1 1 1 1 1 1 1 5 5 5 5 5 5 5 5 5 5 1 1

Inserting the 10 elements from numbers in the second-to-last element position displaces the last two elements in the list to the right. The list now contains 34 elements.

Accessing Elements in a List

You can obtain a reference to the first or last element in a list by calling the front() or back() function for the list. To access elements interior to the list you must use an iterator, and increment or decrement the iterator, to get to the element you want. As you have seen, the begin() and end() functions return a bidirectional iterator pointing at the first element, or one past the last element, respectively. The rbegin() and rend() functions return bidirectional iterators and enable you to iterate through the elements in reverse sequence.

Let's try out some of what we have seen in an example.

Other Operations on Lists

The clear() function deletes all the elements from a list. The erase() function allows you to delete either a single element specified by a single iterator, or a sequence of elements specified by a pair of iterators in the usual fashion — the first in the sequence and one past the last.

int data[] = {10, 22, 4, 56, 89, 77, 13, 9};
list<int> numbers(data, data+8);

 numbers.erase(++numbers.begin());      // Remove the second element

 // Remove all except the first and the last two
numbers.erase(++numbers.begin(), --(--numbers.end()));

Initially, the list will contain all the values from the data array. The first erase() operation deletes the second element, so the list will contain:

10 4 56 89 77 13 9

For the second erase() operation, the first argument is the iterator returned by begin(), incremented by 1, so that it points to the second element. The second argument is the iterator returned by end(), decremented twice, so that it points to the second-to-last element. Of course, this is one past the end of the sequence, so the element that this iterator points to is not included in the set to be deleted, and the list contents after this operation will be:

10 13 9

The remove() function removes the elements from a list that match a particular value. With the numbers list defined as in the previous fragment, you could remove all elements equal to 22 with the following statement:

numbers.remove(22);

The assign() function removes all the elements from a list and copies either a single object into the list a given number of times, or copies a sequence of objects specified by two iterators. Here's an example:

int data[] = {10, 22, 4, 56, 89, 77, 13, 9};
list<int> numbers(data, data+8);

 numbers.assign(10, 99);      // Replace contents by 10 copies of 99

 // Remove all except the first and the last two
numbers.assign(data+1, data+4); // Replace contents by 22 4 56

The assign() function comes in the two overloaded versions illustrated here. The arguments to the first are the count of the number of replacement elements, and the replacement element value. The arguments to the second version are either two iterators or two pointers, specifying a sequence in the way you have already seen.

The unique() function will eliminate adjacent duplicate elements from a list, so if you sort the contents first, applying the function ensures that all elements are unique. Here's an example:

int data[] = {10, 22, 4, 10, 89, 22, 89, 10};
list<int> numbers(data, data+8);       // 10 22 4 10 89 22 89 10
numbers.sort();                        // 4 10 10 10 22 22 89 89
numbers.unique();                      // 4 10 22 89

The result of each operation is shown in the comments.

The splice() function allows you to remove all or part of one list and insert it in another. Obviously, both lists must store elements of the same type. Here's the simplest way you could use the splice() function:

int data[] = {1, 2, 3, 4, 5, 6, 7, 8};
list<int> numbers(data, data+3);                 // 1 2 3
list<int> values(data+4, data+8);                // 5 6 7 8
numbers.splice(++numbers.begin(), values);       // 1 5 6 7 8 2 3

The first argument to the splice() function is an iterator specifying where the elements should be inserted, and the second argument is the list that is the source of the elements to be inserted. This operation removes all the elements from the values list and inserts them immediately preceding the second element in the numbers list.

Here's another version of the splice() function that removes elements from a given position in a source list and inserts them at a given position in the destination list:

int data[] = {1, 2, 3, 4, 5, 6, 7, 8};
list<int> numbers(data, data+3);                           // 1 2 3
list<int> values(data+4, data+8);                          // 5 6 7 8
numbers.splice(numbers.begin(), values, --values.end());   // 8 1 2 3

In this version, the first two arguments to the splice() function are the same as the previous version of the function. The third argument is an iterator specifying the position of the first element to be selected from the source list; all elements, from this position to the end, are removed from the source and inserted in the destination list. After executing this code fragment, values will contain 5 6 7.

The third version of splice() requires four arguments and selects a range of elements from the source list:

int data[] = {1, 2, 3, 4, 5, 6, 7, 8};
list<int> numbers(data, data+3);                 // 1 2 3
list<int> values(data+4, data+8);                // 5 6 7 8
numbers.splice(++numbers.begin(), values, ++values.begin(), --values.end());
                                                 // 1 6 7 2 3

The first three arguments to the version of splice() are the same as the previous version, and the last argument is one past the last element to be removed from the source, values. After executing this, values will contain

5 8.

The merge() function removes elements from the list that you supply as an argument and inserts them in the list for which the function is called. The function then sorts the contents of the extended list into ascending order by default, or into some other order determined by a function object that you supply as a second argument to the merge() function. Both lists must be ordered appropriately before you call merge(); in other words, the lists must be ordered in the way that you want the final combined list to be ordered. Here's a fragment showing how you might use it:

int data[] = {1, 2, 3, 4, 5, 6, 7, 8};
list<int> numbers(data, data+3);                 // 1 2 3
list<int> values(data+1, data+8);                // 2 3 4 5 6 7 8
numbers.merge(values);                           // 1 2 2 3 3 4 5 6 7 8

This merges the contents of values into numbers, so values will be empty after this operation. The merge() function that accepts a single argument orders the result in ascending sequence by default, and because the values in both lists are already ordered, you don't need to sort them. To merge the same lists in descending sequence, the code would be as follows:

numbers.sort(greater<int>());                    // 3 2 1
values.sort(greater<int>());                     // 8 7 6 5 4 3 2
numbers.merge(values, greater<int>());           // 8 7 6 5 4 3 3 2 2 1

Here you use the greater<int>() function object that is defined in the functional header to specify that the lists should be sorted in descending sequence and that they should be merged into the same sequence.

The remove_if() function removes elements from a list based on the result of applying a unary predicate; I'm sure you'll recall that a unary predicate is a function object that applies to a single argument and returns a bool value, true or false. If the result of applying the predicate to an element is true, then the element will be deleted from the list. Typically, you would define your own predicate to do this. This involves defining your own class template for the function object that you want, while the STL defines the unary_function<T, R> base template for use in this context. This template just defines types that will be inherited by the derived class that specifies your function object type. The base class template is defined as follows:

template<class _Arg, class _Result>
struct unary_function
{ // base class for unary functions
  typedef _Arg argument_type;
  typedef _Result result_type;
};

This defines argument_type and result_type as standardized types for use in your definition of the operator()() function. You must use this base template if you want to use your predicates with function adapters.

The way in which you can use the remove_if() function is best explained with a specific application, so let's try this in a working example.

Using Other Sequence Containers

The remaining sequence containers are implemented through container adapters that I introduced at the beginning of this chapter. I'll discuss each of them briefly and illustrate their operation with an examples.

Queue Containers

A queue<T> container implements a first-in first-out storage mechanism through an adapter. You can only add to the end of the queue or remove from the front. Here's one way you can create a queue:

queue<string> names;

This creates a queue that can store elements of type string. By default, the queue<T> adapter class uses a deque<T> container as the base, but you can specify a different sequence container as a base, as long as it supports the operations front(), back(), push_back(), and pop_front(). These four functions are used to operate the queue. Thus, a queue can be based on a list or a vector container. You specify the alternate container as a second template parameter. Here's how you would create a queue based on a list:

queue< string, list<string> > names;

The second type parameter to the adapter template specifies the underlying sequence container that is to be used. The queue adapter class acts as a wrapper for the underlying container class and essentially restricts the range of operations you can carry out to those described in the following table.

FUNCTION

DESCRIPTION

back()

Returns a reference to the element at the back of the queue. There are two versions of the function, one returning a const reference and the other returning a non-const reference. If the queue is empty, then the value returned is undefined.

front()

Returns a reference to the element at the front of the queue. There are two versions of the function, one returning a const reference and the other returning a non-const reference. If the queue is empty, then the value returned is undefined.

push()

Adds the element specified by the argument to the back of the queue.

pop()

Removes the element at the front of the queue.

size()

Returns the number of elements in the queue.

empty()

Returns true if the queue is empty and false otherwise.

Note that there are no functions in the table that make iterators available for a queue container. The only way to access the contents of a queue is via the back() or front() functions.

Priority Queue Containers

A priority_queue<T> container is a queue that always has the largest or highest priority element at the top. Here's one way to define a priority queue container:

priority_queue<int> numbers;

The default criterion for determining the relative priority of elements as you add them to the queue is the standard less<T> function object template. You add an element to the priority queue using the push() function:

numbers.push(99);                      // Add 99 to the queue

When you add an element to the queue, if the queue is not empty the function will use the less<T>() predicate to decide where to insert the new object. This will result in elements being ordered in ascending sequence from the back of the queue to the front. You cannot modify elements while they are in a priority queue, as this could invalidate the ordering that has been established.

The complete set of operations for a priority queue is shown in the following table.

FUNCTION

DESCRIPTION

top()

Returns a const reference to the element at the front of the priority queue, which will be the largest or highest priority element in the container. If the priority queue is empty, then the value returned is undefined.

push()

Adds the element specified by the argument to the priority queue at a position determined by the predicate for the container, which by default is less<T>.

pop()

Removes the element at the front of the priority queue, which will be the largest or highest priority element in the container.

size()

Returns the number of elements in the priority queue.

empty()

Returns true if the priority queue is empty and false otherwise.

Note that there is a significant difference between the functions available for the priority queue and for the queue container. With a priority queue, you have no access to the element at the back of the queue; only the element at the front is accessible.

By default, the base container used by the priority queue adapter class is vector<T>. You have the option of specifying a different sequence container as the base, and an alternative function object for determining the priority of the elements. Here's how you could do that:

priority_queue<int, deque<int>, greater<int>> numbers;

This statement defines a priority queue based on a deque<int> container, with elements being inserted using a function object of type greater<int>. The elements in this priority queue will be in descending sequence, with the smallest element at the top. The three template parameters are the element type, the container to be used as a base, and the type for the predicate to be used for ordering the elements.

You could omit the third template parameter if you want the default predicate to apply, which will be less<int> in this case. If you want a different predicate but want to retain the default base container, you must explicitly specify it, like this:

priority_queue<int, vector<int>, greater<int>> numbers;

This specifies the default base container vector<int> and a new predicate type, greater<int>, to be used to determine the ordering of elements.

Stack Containers

The stack<T> container adapter template is defined in the stack header and implements a pushdown stack based on a deque<T> container by default. A pushdown stack is a last-in first-out storage mechanism where only the object that was added most recently to the stack is accessible.

Here's how you can define a stack:

stack<Person> people;

This defines a stack to store Person objects.

The base container can be any sequence container that supports the operations back(), push_back(), and pop_back(). You could define a stack base on a list like this:

stack<string, list<string>> names;

The first template type argument is the element type, as before, and the second is the container type to be used as a base for the stack.

There are only five operations available with a stack<T> container, and they are shown in the following table.

FUNCTION

DESCRIPTION

top()

Returns a reference to the element at the top of the stack. If the stack is empty, then the value returned is undefined. You can assign the reference returned to a const or non-const reference and if it is assigned to the latter, you can modify the object in the stack.

push()

Adds the element specified by the argument to the top of the stack.

pop()

Removes the element at the top of the stack.

size()

Returns the number of elements in the stack.

empty()

Returns true if the stack is empty and false otherwise.

As with the other containers provided through container adapters, you cannot use iterators to access the contents of a stack.

Let's see a stack working in another example.

The array Container

The array<T, N> container that is defined in the array header stores an array of N elements of type T. The array <> container is very easy to use, and you can initialize it in the same way as an ordinary C++ array. For example:

std::array<int, 10> samples = { 1, 2, 3, 4, 5};

The samples container stores 10 elements of type int. The first five are initialized by the values in the initializer list; the elements without initial values will be zero. If the elements were of a class type, any elements that were not already initialized would be initialized using the no-arg constructor for the element type.

Like the vector<> and list<> containers, you can work through all the elements in an array<> using an iterator:

for(auto iter = samples.begin() ; iter != samples.end() ; ++iter)
  cout << *iter << "  ";

Because it also behaves like an ordinary array, you can also use an index value to access the elements in an array<> container:

for(size_t i = 0 ; i < samples.size() ; ++i)
  cout << samples[i] << "  ";

Elements are indexed from zero, just like an ordinary array. The size() member returns the number of elements in the container, and you just use this in the for loop to control indexing over all the elements.

A further possibility for accessing elements in an array is to use the at() function. The argument is the index position of the element you want to access, and the operation will throw an exception of type out_of_range if the argument is not a valid index. You could therefore write the previous loop as:

for(size_t i = 0 ; i < samples.size() ; ++i)
  cout << samples.at(i) << "  ";

Of course, if you want to deal with an index out-of-range condition you must put the statement in a try block and catch the exception.

There are overloaded operators for the full complement of comparison operators for array<> objects so you can use the operators <, <=, ==, !=, >= and > to compare array<> containers. Array containers are compared element by element, and the first pair of elements that differ determines that the arrays are unequal and whether one array<> object is greater or less than the other. Two array<> objects are equal if they have the same number of elements and corresponding elements are equal.

The tuple<> class template that is defined in the tuple header is a useful adjunct to the array<> container, and to other sequence containers such as the vector<>. As its name suggests, a tuple<> object encapsulates a number of different items of different types. If you were working with fixed records from a file, or possibly from an SQL database, you could use a vector<> or an array<> container to store tuple<> objects that encapsulate the fields in a record. Let's look at a specific example.

Suppose you are working with personnel records that contain an integer employee number, a first name, a second name, and the age of the individual in years. You could define a tuple<> instance to encapsulate an employee record like this:

typedef std::tuple<int, std::string, std::string, int> Record;

A typedef is very useful for reducing the verbosity of the type specification in this case. This typedef defines the Record type as being a tuple<> that stores values of type int, string, string, and int, corresponding to a person's ID, first name, second name, and age. You could now define an array<> container to store Record objects:

std::array<Record, 5> personnel = { Record(1001, "Joan", "Jetson", 35),
                                    Record(1002, "Jim", "Jones", 26),
                                    Record(1003, "June", "Jello", 31),
                                    Record(1004, "Jack", "Jester", 39)};

This defines the personnel object, which is an array that can store five Record objects. If you wanted flexibility in the number of tuples you were dealing with, you could use a vector<> or a list<> container. Here you initialize four of the five elements in personnel from the initializer list. You could add elements explicitly though. Here's how you might add a fifth element:

personnel[4] = Record(1005, "Jean", "Jorell", 29);

This uses the index operator to access the fifth element in the array. You also have the option of creating a tuple<> object using the make_tuple() function template:

personnel[4] = std::make_tuple(1005, "Jean", "Jorell", 29);

The make_tuple() function creates a tuple<> instance that is equivalent to our Record type because the deduced type parameters are the same.

To access the fields in a tuple, you use the get() function template. The template parameter is the index position of the field you want, where fields in a tuple are indexed from zero. The argument to the get() function is the tuple that you are accessing. For example, here's how you could list the records in our personnel container:

cout << std::setiosflags(std::ios::left);      // Left align output
for(auto iter = personnel.begin() ; iter != personnel.end() ; ++iter)
  cout << std::setw(10) << get<0>(*iter) << std::setw(10) << get<1>(*iter)
       << std::setw(10) << get<2>(*iter) << std::setw(10) << get<3>(*iter)
       << endl;

This outputs the fields in each tuple in personnel on a single line, where the fields are left-justified in a field width of 10 characters, so it looks tidy. Note that the type parameter to the get() function template must be a compile-time constant. This implies that you cannot use a loop index variable as the type parameter, so you can't iterate over the fields in a loop. It is helpful to define some integer constants that you can use to identify the fields in a tuple in a more readable way. For example, you could define the following constants:

const size_t ID(0), firstname(1), secondname(2), age(3);

Now you can use these variables as type parameters in get() function instantiations making it much clearer which field you are retrieving.

Let's put some of the fragments together into something you can compile and run.

ASSOCIATIVE CONTAINERS

The most significant feature of the associative containers such as map<K, T> is that you can retrieve a particular object without searching. The location of an object of type T within an associative container is determined from a key of type K that you supply along with the object, so you can retrieve any object rapidly just by supplying the appropriate key. The key is actually a sort key that determines the order of the entries in the map.

For set<T> and multiset<T> containers, objects act as their own keys. You might be wondering what the use of a container is if, before you can retrieve an object, you have to have the object available. After all, if you already have the object, why would you need to retrieve it? The point of set and multiset containers is not so much to store objects for later retrieval, but to create an aggregation of objects that you can test to see whether or not a given object is already a member.

In this section, I'll concentrate on map containers. The set and multiset containers are used somewhat less frequently, and their operations are very similar to the map and multimap containers, so you should have little difficulty using these once you have learned how to apply the map containers.

Using Map Containers

The template for map containers is defined in the map header. When you create a map<K, T> container, you must supply type arguments for the type of key you will use, K, and the type of the object associated with a key, T. Here's an example:

map<Person, string> phonebook;

This defines an empty map container that stores entries that are key/object pairs, where the keys are of type Person and the objects are of type string.

Note

Although you use class objects here for both keys and objects to be stored in the map, the keys and associated objects in a map can also be of any fundamental type, such as int, or double or char.

You can also create a map container that is initialized with a sequence of key/object pairs from another map container:

map<Person, string> phonebook(iter1, iter2);

iter1 and iter2 are a pair of iterators defining a series of key/object pairs from another container in the usual way, with iter2 specifying a position one past the last pair to be included in the sequence. You obtain iterators to access the contents of a map by calling the begin() and end() functions, just as you would for a sequence container. The iterators for a map are bidirectional iterators.

The entries in a map are ordered based on a function object of type less<Key> by default, so they will be stored in ascending key sequence. You can change the type function object used for ordering entries in a map by supplying a third template type parameter. For example:

map<Person, string, greater<Person>> phonebook;

This map stores entries that are Person/string pairs, where Person is the key with an associated string object. The ordering of entries will be determined by a function object of type greater<Person>, so the entries will be in descending key sequence.

Storing Objects

The objects that you store in a map are always a key/object pair that are of a template type pair<K, T>, where K is the type of key and T is the type of object associated with the key. The pair<K, T> type is defined in the utility header, which is included in the map header, so if you are using a map the type is automatically available. You can define a pair object like this:

auto entry = pair<Person, string>(Person("Mel", "Gibson"), "213 345 5678");

This creates the variable entry of type pair<Person,string> and initializes it to an object created from a Person object and a string object. I'm representing a phone number in a very simplistic way, just as a string, but of course it could be a more complicated class identifying the components of the number, such as country code and area code. The Person class is the class you used in the previous example.

An instance of the pair<K, T> class template defines two constructors, and the one you are using in the previous fragment defines an object from a key and its associated object. The other constructor is a copy constructor that allows you to construct a new pair from an existing one. You can access the elements in a pair through the members first and second; thus, in the example, entry.first references the Person object and entry.second references the string object.

You can also use a helper function, make_pair(), that is defined in the utility header to create a pair object:

auto entry = make_pair(Person("Mel", "Gibson"), "213 345 5678");

The make_pair() function is defined as a template, so it automatically deduces the type for the pair, from the argument types you supply.

All of the comparison operators are overloaded for pair objects, so you can compare them with any of the operators <, <=, ==, !=, >=, and >.

It's sometimes convenient to use a typedef statement to abbreviate the pair<K, T> type you are using in a particular instance. For example:

map<Person, string> phonebook;
typedef pair<Person, string> Entry;

The first statement defines a map container that will store string objects using Person objects as keys. The second statement defines Entry as the type for the key/object pair. Having defined the Entry type, you can create objects of this type. For example:

Entry entry1 = Entry(Person("Jack", "Jones"), "213 567 1234");

This statement defines a pair of type pair<Person, string>, using Person("Jack", "Jones") as the Person argument and "213 567 1234" as the string argument.

You can insert one or more pairs in a map using the insert() function. For example, here's how you insert a single object:

phonebook.insert(entry1);

This statement inserts the entry1 pair into the phonebook container, as long as there is no other entry in the map that uses the same key. In fact, this version of the insert() function returns a value that is also a pair, where the first object in the pair is an iterator and the second is a value of type bool. The bool value in the pair will be true if the insertion was made, and false otherwise. The iterator value in the pair will point to the element if it was stored in the map, or the element that is already in the map if the insert failed. Therefore, you can check if the object was stored, like this:

auto checkpair = phonebook.insert(entry1);
if(checkpair.second)
  cout << "Insertion succeeded." << endl;
else
  cout << "Insertion failed." << endl;

The pair that the insert() function returns is stored in checkpair. The type for checkpair is pair<map<Person,string>::iterator, bool>, which corresponds to a pair encapsulating an iterator for our map of type map<Person,string>::iterator, which you could access as checkpair.first; and a value of type bool, which you access in the code as checkpair.second.

Dereferencing the iterator in the pair returned by the insert() function will give you access to the pair that is stored in the map; you can use the first and second members of that pair to access the key and object, respectively. This can be a little tricky, so let's see what it looks like for checkpair in the code above:

cout << "The key for the entry is:" << endl;
checkpair.first->first.showPerson();

The expression checkpair.first references the first member of the checkpair pair, which is an iterator, so you are accessing a pointer to the object in the map with this expression. The object in the map is another pair, so the expression checkpair.first->first accesses the first member of that pair, which is the Person object. You use this to call the showPerson() member to output the name. You could access the object in the pair in a similar way, with the expression checkpair.first->second.

You have another version of the insert() function for inserting a series of pairs in a map. The pairs are defined by two iterator arguments, and the series would typically be from another map container.

The map<K,T> template defines the operator[]() function, so you can also use the subscript operator to insert an object. Here's how you could insert the entry1 object in the phonebook map:

phonebook[Person("Jack", "Jones")] = "213 567 1234";

The subscript value is the key to be used to store the object that appears to the right of the assignment operator. This is perhaps a somewhat more intuitive way to store objects in a map. The only disadvantage, compared to the insert() function, is that you lose the ability to discover whether the key was already in the map.

Accessing Objects

You can use the subscript operator to retrieve the object from a map that corresponds to a given key. For example:

string number = phonebook[Person("Jack", "Jones")];

This stores the object corresponding to the key

Person("Jack", "Jones")

in number. If the key is not in the map, then a pair entry will be inserted into the map for this key, with the object as the default for the object type; here, the no-arg Person class constructor will be called to create the object for this key if the entry is not there.

Of course, you may not want a default object inserted when you attempt to retrieve an object corresponding to a given key. In this case, you could use the find() function to check whether there's an entry for a given key, and then retrieve it:

string number;
Person key = Person("Jack", "Jones");
auto iter = phonebook.find(key);

 if(iter != phonebook.end())
{
  number = iter->second;
  cout << "The number is " << number << endl;
}
else
{
  cout << "No number for the key ";
  key.showPerson();
}

The find() function returns an iterator of type map<Person,string>::iterator that points to the object corresponding to the key if the key is present in the map, or to one past the last entry in the map, which corresponds to the iterator returned by the end() function. Thus, if iter is not equal to the iterator returned by end(), the entry is present, and you can access the object through the second member of the pair. If you want to prevent the object in the map from being modified, you could define the iterator type explicitly as map<Person,string>::const_iterator.

Calling the count() function for a map with a key as the argument will return a count of the number of entries found corresponding to the key. For a map, the value returned can only be 0 or 1, because each key in a map must be unique. A multimap container allows multiple entries for a given key, so in this case other values are possible for the return value from count().

Other Map Operations

The erase() function enables you to remove a single entry, or a range of entries, from a map. You have two versions of erase() that will remove a single entry. One version requires an iterator as the argument pointing to the entry to be erased, and the other requires a key corresponding to the entry to be erased. For example:

Person key = Person("Jack", "Jones");
auto count = phonebook.erase(key);
if(count == 0)
  cout << "Entry was not found." << endl;

When you supply a key to the erase() function, it returns a count of the number of entries that were erased. With a map container, the value returned can only be 0 or 1. A multimap container can have several entries with the same key, in which case the erase() function may return a value greater than 1.

You can also supply an iterator as an argument to erase():

Person key = Person("Jack", "Jones");
auto iter = phonebook.find(key);
iter = phonebook.erase(iter);
if(iter == phonebook.end())
  cout << "End of the map reached." << endl;

In this case, the erase() function returns an iterator that points to the entry that remains in the map beyond the entry that was erased, or a pointer to the ends of the map if no such element is present.

The following table shows the other operations available with a map container.

FUNCTION

DESCRIPTION

begin()

Returns a bidirectional iterator pointing to the first entry in the map.

end()

Returns a bidirectional iterator pointing to one past the last entry in the map.

rbegin()

Returns a reverse iterator pointing to the last entry in the map.

rend()

Returns a reverse iterator pointing to one before the first entry in the map.

lower_bound()

Accepts a key as an argument and returns an iterator pointing to the first entry with a key that is greater than or equal to (the lower bound of) the specified key. If the key is not present, the iterator pointing to one past the last entry will be returned.

upper_bound()

Accepts a key as an argument and returns an iterator pointing to the first entry with a key that is greater than (the upper bound of) the specified key. If the key is not present, the iterator pointing to one past the last entry will be returned.

equal_range()

Accepts a key as an argument and returns a pair object containing two iterators. The first member of the pair points to the lower bound of the specified key, and the second member points to the upper bound of the specified key. If the key is not present, both iterators in the pair will point to one past the last entry in the map.

swap()

Interchanges the entries in the map you pass as the argument, with the entries in the map for which the function is called.

clear()

Erases all entries in the map.

size()

Returns the number of elements in the map.

empty()

Returns true if the map is empty and false otherwise.

The lower_bound(), upper_bound(), and equal_range() functions are not very useful with a map container. However, they come into their own with a multimap container, when you want to find all the elements with the same key.

Let's see a map in action.

Using a Multimap Container

A multimap container works very much like the map container, in that it supports the same range of functions — except for the subscript operator, which you cannot use with a multimap. The principle difference between a map and a multimap is that you can have multiple entries with the same key in a multimap, and this affects the way some of the functions behave. Obviously, with the possibility of several keys having the same value, overloading the operator[]() function would not make much sense for a multimap.

The insert() function flavors for a multimap are a little different from the insert() functions for a map. The simplest version of insert() that accepts a pair<K, T> object as an argument, returns an iterator pointing to the entry that was inserted in the multimap. The equivalent function for a map returns a pair object, because this provides an indication of when the key already exists in the map and the insertion is not possible; of course, this cannot arise with a multimap. A multimap also has a version of insert() with two arguments: the second being the pair to be inserted, and the first being an iterator pointing to the position in the multimap from which to start searching for an insertion point. This gives you some control over where a pair will be inserted when the same key already exists. This version of insert() also returns an iterator pointing to the element that was inserted. The third version of insert() accepts two iterator arguments that specify a range of elements to be inserted from some other source.

When you pass a key to the erase() function for a multimap, it erases all entries with the same key, and the value returned indicates how many entries were deleted. The significance of having a version of erase() available that accepts an iterator as an argument should now be apparent — it allows you to delete a single element. You also have a version of erase() available that accepts two iterators to delete a range of entries.

The find() function can only find the first element with a given key in a multimap. You really need a way to find several elements with the same key and the lower_bound(), upper_bound(), and equal_range() functions provide you with a way to do this. For example, given a phonebook object that is type

multimap<Person, string>

rather than type map<Person, string>, you could list the phone numbers corresponding to a given key like this:

Person person = Person("Jack", "Jones");
auto iter = phonebook.lower_bound(person);
if(iter == phonebook.end())
  cout << "There are no entries for " << person.getName() << endl;
else
{
  cout << "The following numbers are listed for " << person.getName() << ":"
       << endl;
  for(  ;  iter != phonebook.upper_bound(person) ; iter++)
     cout << iter->second << endl;
}

It's important to check the iterator returned by the lower_bound() function. If you don't, you could end up trying to reference an entry one beyond the last entry.

MORE ON ITERATORS

The iterator header defines several templates for iterators that transfer data from a source to a destination. Stream iterators act as pointers to a stream for input or output, and they enable you to transfer data between a stream and any source or destination that works with iterators, such as an algorithm. Inserter interators can transfer data into a basic sequence container. The iterator header defines two stream iterator templates, istream_iterator<T> for input streams and ostream_iterator<T> for output streams, where T is the type of object to be extracted from, or written to, the stream. The header also defines three inserter templates, inserter<T>, back_inserter<T> and front_inserter<T>, where T is the type of sequence container in which data is to be inserted.

Let's explore some of these iterators in a little more depth.

Using Input Stream Iterators

Here's an example of how you create an input stream iterator:

istream_iterator<int> numbersInput(cin);

This creates the iterator numbersInput of type istream_iterator<int> that can point to objects of type int in a stream. The argument to the constructor specifies the actual stream to which the iterator relates, so this is an iterator that can read integers from cin, the standard input stream.

The default istream_iterator<T> constructor creates an end-of-stream iterator, which will be equivalent to the end iterator for a container that you have been obtaining by calling the end() function. Here's how you could create an end-of-stream iterator for cin, complementing the numbersInput iterator:

istream_iterator<int> numbersEnd;

Now you have a pair of iterators that defines a sequence of values of type int from cin. You could use these to load values from cin into a vector<int> container:

vector<int> numbers;
cout << "Enter integers separated by spaces then a letter to end:" << endl;
istream_iterator<int> numbersInput(cin), numbersEnd;
while(numbersInput != numbersEnd)
  numbers.push_back(*numbersInput++);

After defining the vector container to hold values of type int, you create two input stream iterators: numbersInput is an input stream iterator reading values of type int from cin, and numbersEnd is an end-of-stream iterator for the same input stream. The while loop continues as long as numbersInput is not equal to the end-of-stream iterator, numbersEnd. When you execute this fragment, input continues until end-of-stream is recognized for cin. But what produces that condition? The end-of-stream condition will arise if you enter Ctrl+Z to close the input stream, or if you enter an invalid character such as a letter.

Of course, you are not limited to using input stream iterators as loop control variables. You can use them to pass data to an algorithm, such as accumulate(), which is defined in the numeric header:

cout << "Enter integers separated by spaces then a letter to end:" << endl;
istream_iterator<int> numbersInput(cin), numbersEnd;
cout << "The sum of the input values that you entered is "
     << accumulate(numbersInput, numbersEnd, 0) << endl;

This fragment outputs the sum of however many integers you enter. You will recall that the arguments to the accumulate() algorithm are an iterator pointing to the first value in the sequence, an iterator pointing to one past the last value, and the initial value for the sum. Here you are transferring data directly from cin to the algorithm.

The sstream header defines the basic_istringstream<T> type that defines an object type that can access data from a stream buffer such as a string object. The header also defines the istringstream type as basic_istringstream<char>, which will be a stream of characters of type char. You can construct an istringstream object from a string object, which means you can read data from the string object, just as you read from cin. Because an istringstream object is a stream, you can pass it to an input iterator constructor and use the iterator to access the data in the underlying stream buffer. Here's an example of how you do that:

string data("2.4 2.5 3.6 2.1 6.7 6.8 94 95 1.1 1.4 32");
istringstream input(data);
istream_iterator<double> begin(input), end;
cout << "The sum of the values from the data string is "
     << accumulate(begin, end, 0.0) << endl;

You create the istringstream object, input, from the string object, data, so you can read from data as a stream. You create two stream iterators that can access double values in the input stream, and you use these to pass the contents of data to the accumulate() algorithm. Note that the type of the third argument to the accumulate() function determines the type of the result, so you must specify this as a value of type double to get the sum produced correctly.

Let's try a working example.

Using Inserter Iterators

An inserter iterator is an iterator that can add new elements to the sequence containers vector<T>, deque<T>, and list<T>. There are three templates that create inserter iterators:

  • back_insert_iterator<T> — inserts elements at the end of a container of type T. The container must provide the push_back() function for this to work.

  • front_insert_iterator<T> — inserts elements at the beginning of a container of type T. This depends on push_front() being available for the container.

  • insert_iterator<T> — inserts elements starting at a specified position within a container of type T. This requires that the container has an insert() function that accepts an iterator as the first argument, and an item to be inserted as the second argument.

The constructors for the first two types of inserter iterators expect a single argument specifying the container in which elements are to be inserted. For example:

list<int> numbers;
front_insert_iterator< list<int> > iter(numbers);

Here you create an inserter iterator that can insert data at the beginning of the list<int> container numbers.

Inserting a value into the container is very simple:

*iter = 99;                  // Insert 99 at the front of the numbers container

You could also use the front_inserter() function with the numbers container, like this:

front_inserter(numbers) = 99;

This creates a front inserter for the numbers list and uses it to insert 99 at the beginning of the list. The argument to the front_inserter() function is the container to which the iterator is to be applied.

The constructor for an insert_iterator<T> iterator requires two arguments:

insert_iterator<vector<int>> iter_anywhere(numbers, numbers.begin());

The second argument to the constructor is an iterator specifying where data is to be inserted — the start of the sequence, in this instance. You can use this iterator in exactly the same way as the previous one. Here's how you could insert a series of values into a vector container using this iterator:

for(int i = 0 ; i<100 ; i++)
  *iter_anywhere = i + 1;

This loop inserts the values from 1 to 100 in the numbers container at the beginning. After executing this, the first 100 elements will be 100, 99, and so on, through to 1.

You could also use the inserter() function to achieve the same result:

for(int i = 0 ; i<100 ; i++)
  inserter(numbers, numbers.begin()) = i + 1;

The first argument to inserter() is the container, and the second is an iterator identifying the position where data is to be inserted. The inserter iterators can be used in conjunction with the copy() algorithm in a particularly useful way. Here's how you could read values from cin and transfer them to a list<T> container:

list<double> values;
cout << "Enter a series of values separated by spaces"
     << " followed by Ctrl+Z or a letter to end:" << endl;
istream_iterator<double> input(cin), input_end;
copy(input, input_end, back_inserter<list<double>>(values));

You first create a list container that stores double values. After a prompt for input, you create two input stream iterators for values of type double. The first iterator points to cin, and the second iterator is an end-of-stream iterator created by the default constructor. You specify the input to the copy() function with the two iterators; the destination for the copy operation is a back inserter iterator that you create in the third argument to the copy() function. The back inserter iterator adds the data transferred by the copy operation to the list container, values. This is quite powerful stuff. If you ignore the prompt, in three statements you can read an arbitrary number of values from the standard input stream and transfer them to a list container.

Using Output Stream Iterators

Complementing the input stream iterator template, the ostream_iterator<T> template provides output stream iterators for writing objects of type T to an output stream. There are two constructors for creating an instance of the output stream iterator template. One creates an iterator that just transfers data to the destination stream:

ostream_iterator<int> out(cout);

The type argument, int, to the template, specifies the type of data to be handled, while the constructor argument, cout, specifies the stream that will be the destination for data, so the out iterator can write value of type int to the standard output stream. Here's how you might use this iterator:

int data[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
vector<int> numbers(data, data+9);     // Contents 1 2 3 4 5 6 7 8 9
copy(numbers.begin(), numbers.end(), out);

The copy() algorithm that is defined in the algorithm header copies the sequence of objects specified by the first two iterator arguments to the output iterator specified by the third argument. Here, the function copies the elements from the numbers vector to the out iterator, which will write the elements to cout. The result of executing this fragment will be:

123456789

As you can see, the values are written to the standard output stream with no spaces in between. The second output stream iterator constructor can improve on this:

ostream_iterator<int>  out(cout, ", ");

The second argument to the constructor is a string to be used as a delimiter for output values. If you use this iterator as the third argument to the copy() function in the previous fragment, the output will be:

1, 2, 3, 4, 5, 6, 7, 8, 9,

The delimiter string that you specify as a second constructor argument is written to the stream following each value.

Let's see how an output stream iterator works in practice.

MORE ON FUNCTION OBJECTS

The functional header defines an extensive set of templates for creating function objects that you can use with algorithms and containers. I won't discuss them in detail, but I'll summarize the most useful ones. The function objects for comparisons are shown in the following table.

FUNCTION OBJECT TEMPLATE

DESCRIPTION

less<T>

Creates a binary predicate representing the < operation between objects of type T. For example, less<string>() defines a function object for comparing objects of type string.

less_equal<T>

Creates a binary predicate representing the <= operation between objects of type T. For example, less_equal<double>() defines a function object for comparing objects of type double.

equal<T>

Creates a binary predicate representing the == operation between objects of type T.

not_equal<T>

Creates a binary predicate representing the != operation between objects of type T.

greater_equal<T>

Creates a binary predicate representing the >= operation between objects of type T.

greater<T>

Creates a binary predicate representing the > operation between objects of type T.

not2<B>

Creates a binary predicate that is the negation of a binary predicate of type B. For example, not2(less<int>) creates a binary predicate for comparing objects of type int that returns true if the left operand is not less than the right operand. The template type parameter value B is deduced from the type of the constructor argument.

Here's how you could use the not2<B> template to define a binary predicate for use with the sort() algorithm:

sort(v.begin(), v.end(), not2(greater<string>()));

The argument to the not2 constructor is greater<string>(), which is a call to the constructor for the greater<string> class type, so the sort() function will sort using "not greater than" as the comparison between objects in the container, v.

The functional header also defines function objects for performing arithmetic operations on elements. You would typically use these to apply operations to sequences of numerical values using the transform() algorithm that is defined in the algorithm header. These function objects are described in the following table, where the parameter T specifies the type of the operands.

FUNCTION OBJECT TEMPLATE

DESCRIPTION

plus<T>

Calculates the sum of two elements of type T.

minus<T>

Calculates the difference between two elements of type T by subtracting the second operand from the first.

multiplies<T>

Calculates the product of two elements of type T.

divides<T>

Divides the first operand of type T by the second operand of type T.

modulus<T>

Calculates the remainder after dividing the first operand of type T by the second.

negate<T>

Returns the negative of the operand of type T.

To make use of these you need to apply the transform() function, and I'll explain how this works in the next section.

MORE ON ALGORITHMS

The algorithm and numeric headers define a large number of algorithms. The algorithms in the numeric header are primarily devoted to processing arrays of numerical values, whereas those in the algorithm header are more general purpose and provide such things as the ability to search, sort, copy, and merge sequences of objects specified by iterators. There are far too many to discuss in detail in this introductory chapter, so I'll just introduce a few of the most useful algorithms from the algorithm header to give you a basic idea of how they can be used.

You have already seen the sort() and copy() algorithms from the algorithm header in action. Let's take a brief look at a few other of the more interesting functions in the algorithm header.

fill()

The fill() function is of this form:

fill(ForwardIterator begin, ForwardIterator end, const Type& value)

This fills the elements specified by the iterators begin and end with value. For example, given a vector v, storing values of type string containing more than 10 elements, you could write:

fill(v.begin(), v.begin()+10, "invalid");

This would set the first 10 elements in v to "invalid", the value specified by the last argument to fill().

replace()

The replace() algorithm is of the form:

replace(ForwardIterator begin, ForwardIterator end,
                                      const Type& oldValue, const Type& newValue)

This function examines each element in the range specified by begin and end, and replaces each occurrence of oldValue by newValue. Given a vector v that stores string objects, you could replace occurrences of "yes" by "no" with the following statement:

replace(v.begin(), v.end(), string("yes"), string("no"));

Like all the algorithms that receive an interval defined by a couple of iterators, the replace() function will also work with pointers. For example:

char str[] =  "A nod is as good as a wink to a blind horse.";
replace(str, str+strlen(str), 'o', '*'),
cout << str << endl;

This will replace every occurrence of 'o' in the null-terminated string str by '*', so the result of executing this fragment will be the output:

A n*d is as g**d as a wink t* a blind h*rse.

find()

The find() function is of the form:

find(InputIterator begin, InputIterator end, const Type& value)

This function searches the sequence specified by the first two arguments for the first occurrence of value. For example, given a vector v containing values of type int, you could write:

vector<int>::iterator iter = find(v.begin(), v.end(), 21);

Obviously, by using iter as the starting point for a new search, you could use the find() algorithm repeatedly to find all occurrences of a given value. Perhaps like this:

vector<int>::iterator iter = v.begin();
int value = 21, count = 0;
while((iter = find(iter, v.end(), value)) != v.end())
{
  iter++;
  count++;
}
cout << "The vector contains " << count << " occurrences of " << value << endl;

This fragment searches the vector v for all occurrences of value. On the first loop iteration, the search starts at v.begin(). On subsequent iterations, the search starts at one past the previous position that was found. The loop will accumulate the total number of occurrences of value in v. You could also code the loop as a for loop:

for((iter = find(v.begin(), v.end(), value)); iter != v.end() ;
                                  (iter = find(iter, v.end(), value))++, count++);

Now the find operation is in the third loop control expression, and you increment iter after the result from the find() function is stored. In my view, the while loop is a better solution because it's easier to understand.

transform()

The transform() function comes in two versions. The first version applies an operation specified by a unary function object to a set of elements specified by a pair of iterators, and is of the form:

transform(InputIterator begin, InputIterator end,
                                       OutputIterator result, UnaryFunction f)

This version of transform() applies the unary function f to all elements in the range specified by the iterators begin and end, and stores the results, beginning at the position specified by the iterator result. The result iterator can be the same as begin, in which case the results will replace the original elements. The function returns an iterator that is one past the last result stored.

Here's an example:

double values[] = { 2.5, −3.5, 4.5, −5.5, 6.5, −7.5};
vector<double> data(values, values+sizeof values/sizeof values[0]);
transform(data.begin(), data.end(), data.begin(), negate<double>());

The transform() function call applies a negate<double> function object to all the elements in the vector data. The results are stored back in data and overwrite the original values; so, after this operation the vector will contain:

−2.5, 3.5, −4.5, 5.5, −6.5, 7.5

Because the operation writes the results back to the data vector, the transform() function will return the iterator data.end().

The second version of transform() applies a binary function, with the operands coming from two ranges specified by iterators. The function is of the form:

transform(InputIterator1 begin1, InputIterator1 end1, InputIterator2 begin2,
                                       OutputIterator result, BinaryFunction f)

The range specified by begin1 and end1 represents the set of left operands for the binary function f that is specified by the last argument. The range representing the right operands starts at the position specified by the begin2 iterator; an end iterator does not need to be supplied for this range because there must be the same number of elements as in the range specified by begin1 and end1. The results will be stored in the range, starting at the result iterator position. The result iterator can be the same as begin1 if you want the results stored back in that range, but it must not be any other position between begin1 and end1. Here's an example of how you might use this version of the transform() algorithm:

double values[] = { 2.5, −3.5, 4.5, −5.5, 6.5, −7.5};
vector<double> data(values, values+ sizeof values/sizeof values[0]);
vector<double> squares(data.size());
transform(data.begin(), data.end(), data.begin(),
                                           squares.begin(), multiplies<double>());
ostream_iterator<double> out(cout, " ");
copy(squares.begin(), squares.end(), out);

You initialize the data vector with the contents of the values array. You then create a vector squares to store the results of the transform() operation with the same number of elements as data. The transform() function uses the multiplies<double>() function object to multiply each element of data by itself. The results are stored in the squares vector. The last two statements use an output stream iterator to list the contents of squares, which will be:

6.25 12.25 20.25 30.25 42.25 56.25

LAMBDA EXPRESSIONS

A lambda expression is a C++ language capability that provides you with an alternative programming mechanism to function objects in many instances. Lambda expressions are not specific to STL, but they can be applied extensively in this context, which is why I chose to discuss them here. A lambda expression defines a function object without a name, and without the need for an explicit class definition. You would typically use a lambda expression as the means of passing a function as an argument to another function. A lambda expression is much easier to use and understand, and requires less code, than defining and creating a regular function object. Of course, it is not a replacement for function objects in general.

Let's consider an example. Suppose you want to calculate the cubes (x3) of the elements in a vector containing numerical values. You could use the transform() operation for this, except that there is no function object to calculate cubes. You can easily create a lambda expression to do it, though:

double values[] = { 2.5, −3.5, 4.5, −5.5, 6.5, −7.5};
vector<double> data(values, values+6);
vector<double> cubes(data.size());
transform(data.begin(), data.end(), cubes.begin(),
                                [](double x){ return x*x*x;} );

The last statement uses the transform() operation to calculate the cubes of the elements in the vector data and store the result in the vector cubes. The lambda expression is the last argument to the transform()function:

[](double x){ return x*x*x;}

The opening square brackets are called the lambda introducer, because they mark the beginning of the lambda expression. There's a bit more to this, but I'll come back to that a little later in this chapter.

The lambda introducer is followed by the lambda parameter list between parentheses, just like a normal function. In this case, there's just a single parameter, x. There are restrictions on the lambda parameter list, compared to a regular function: You cannot specify default values for the parameters to a lambda expression, and the parameter list cannot be of a variable length.

Finally, in the example, you have the body of the lambda expression between braces, again just like a normal function. In this case the body is just a single return statement, but in general, it can consist of as many statements as you like, just like a normal function. I'm sure you have noticed that there is no return type specification. When the body of the lambda expression is a single return statement that returns a value in the body, the return type defaults to that of the value returned. Otherwise, the default return type is void. Of course, you can specify the return type. To specify the return type you would write the lambda expression like this:

[](double x)->double { return x*x*x;}

The type double following the arrow specifies the return type as such.

If you wanted to output the values calculated, you could do so in the lambda expression, like this:

[](double x)->double {
                      double result(x*x*x);
                      cout << result << "  ";
                      return result;
                     }

This just extends the body of the lambda to provide for writing the value that is calculated to the standard output stream. In this case, the return type must be specified because the default return type would be void.

The Capture Clause

The lambda introducer can contain a capture clause that determines how the body of the lambda can access variables in the enclosing scope. In the previous section, the lambda expression had nothing between the square brackets, indicating that no variables in the enclosing scope can be accessed within the body of the lambda. The first possibility is to specify a default capture clause that applies to all variables in the enclosing scope. You have two options for this. If you place = between the square brackets, the body has access to all automatic variables in the enclosing scope by value — i.e., the values of the variables are made available within the lambda expression, but the original variables cannot be changed. On the other hand, if you put & between the square brackets, all the variables in the enclosing scope are accessible by reference, and therefore can be changed by the code in the body of the lambda. Look at this code fragment:

double factor = 5.0;
double values[] = { 2.5, −3.5, 4.5, −5.5, 6.5, −7.5};
vector<double> data(values, values+6);
vector<double> cubes(data.size());
transform(data.begin(), data.end(), cubes.begin(),
           [=](double x){ return factor*x*x*x;} );

In this fragment, the = capture clause allows all the variables in scope to be accessible by value from within the body of the lambda. Note that this is not quite the same as passing arguments by value. The value of the variable factor is available within the lambda, but you cannot update the copy of factor because it is effectively const. The following transform() statement will not compile, for example:

transform(data.begin(),data.end(),cubes.begin(),
           [=](double x)-> double{ factor += 10.0;          // Not allowed!
                                   return factor*x*x*x;} );

If you want to modify the temporary copy of a variable in scope from within the lambda, adding the mutable keyword will enable this:

transform(data.begin(),data.end(),cubes.begin(),
           [=](double x)mutable -> double{ factor += 10.0;          // OK
                                   return factor*x*x*x;} );

Now you can modify the copy of any variable within the enclosing scope without changing the original variable. After executing this statement, the value of factor will still be 5.0. The lambda remembers the local value of factor from one call to the next, so for the first element factor will be 5+10=15, for the second element it will be 15+10=25, and so on.

If you do want to change the original value of factor from within the lambda, just use & as the capture clause:

transform(data.begin(),data.end(),cubes.begin(),
             [&](double x)->double{ factor += 10.0;    // Changes original variable
                                    return factor*x*x*x;} );
cout << "factor = " << factor << endl;

You don't need the mutable keyword here. All variables within the enclosing scope are available by reference, so you can use and alter their values. The result of executing this will be that factor has the value 65. If you thought it was going to be 15.0, remember that the lambda expression executes once for each element in data, and the cubes of the elements will be multiplied by successive values of factor from 15.0 to 65.0.

Capturing Specific Variables

There may be circumstances where you want to be more selective about how a lambda expression accesses variables in the enclosing scope. In this case, you can explicitly identify the variables to be accessed. You could rewrite the previous transform() statement as:

transform(data.begin(), data.end(), cubes.begin(),
      [&factor](double x)->double{ factor += 10.0;    // Changes original variable
                                   return factor*x*x*x;} );

Now, factor is the only variable from the enclosing scope that is accessible, and it is available by reference. If you omit the &, factor would be available by value and not updatable. If you want to identify multiple variables in the capture clause, you just separate them with commas. You could include = in the capture clause list, as well as explicit variable names. For example, with the capture clause [=,&factor] the lambda would have access to factor by reference and all other variables in the enclosing scope by value. Conversely, putting [&,factor] as the capture clause would capture factor by value and all other variables by reference. Note that in a function member of a class, you can include the this pointer in the capture clause for a lambda expression, which allows access to the other functions and data members that belong to the class.

A lambda expression can also include a throw() exception specification that indicates that the lambda does not throw exceptions. Here's an example:

transform(data.begin(), data.end(), cubes.begin(),
      [&factor](double x)throw() −>double{ factor += 10.0;
                                   return factor*x*x*x;} );

If you want to include the mutable specification, as well as the throw() specification, the mutable keyword must precede throw() and must be separated from it by one or more spaces.

Templates and Lambda Expressions

You can use a lambda expression inside a template. Here's an example of a function template that uses a lambda expression:

template <class T>
T average(const vector<T>& vec)
{
  T sum(0);
  for_each(vec.begin(), vec.end(),
    [&sum](const T& value){ sum += value; });
  return sum/vec.size();
}

This template generates functions for calculating the average of a set of numerical values stored in a vector. The lambda expression uses the template type parameter in the lambda parameter specification, and accumulates the sum of all the elements in a vector. The sum variable is accessed from the lambda by reference, so it is able to accumulate the sum there. The last line of the function returns the average, which is calculated by dividing sum by the number of elements in the vector. Let's try an example.

Wrapping a Lambda Expression

An extension to the STL functional header defines the function<> class template that you can use to define a wrapper for a function object; this includes a lambda expression. The function<> template is referred to as a polymorphic function wrapper because an instance of the template can wrap a variety of function objects with a given parameter list and return type. I won't discuss all the details of the function<> class template, but I'll just show how you can use it to wrap a lambda expression. Using the function<> template to wrap a lambda expression effectively gives a name to the lambda expression, which not only provides the possibility of recursion within a lambda expression, it also allows you to use the lambda expression in multiple statements or pass it to several different functions. The same function wrapper could also wrap different lambda expressions at different times.

To create a wrapper object from the function<> template you need to supply information about the return type, and the types of any parameters, to a lambda expression. Here's how you could create an object of type function that can wrap a function object that has one parameter of type double and returns a value of type int:

function<int(double)> f = [](double x)->int{ return static_cast<int>(x*x); };

The type specification for the function template is the return type for the function object to be wrapped, followed by its parameter types between parentheses, and separated by commas. Let's try a working example.

THE STL FOR C++/CLI PROGRAMS

The STL/CLR library is an implementation of the STL for use with C++/CLI programs and the CLR. The STL/CLR library covers all the capability that I have described for the STL for standard C++, so I won't go over the same ground again. I'll simply highlight some of the differences and illustrate how you use the STL/CLR with examples.

The STL/CLR library is contained within the cliext namespace, so all STL names are qualified by cliext rather than std. There are cliext include subdirectories that are equivalents for each of the standard C++ STL headers, including those for container adapters, algorithms and function objects, and the STL/CLR subdirectory name is the same in every case. Thus, the C++/CLI equivalents to the templates defined in a standard STL header, such as functional, can be found in cliext/functional. So, for example, if you want to use vector containers in your C++/CLI program you, need an #include directive for cliext/vector; to use the queue<T> adapter you must include the header file cliext/queue.

STL/CLR Containers

STL/CLR containers can store reference types, handles to reference types, and unboxed value types; they cannot store boxed value types. Most of the time, you will use STL/CLR containers with handles or value types. If you do choose to store reference types in a container, they will be passed by value, and the requirements for storing such objects in an STL/CLR container are essentially the same as for a native STL container. Any reference type stored in an STL/CLR container must at least implement a public copy constructor, a public assignment operator, and a public destructor. These requirements do not apply when you are storing value types or handles to reference types in an STL/CLR container.

Don't forget that the compiler does not supply default versions of the copy constructor and assignment operator for reference types, so when they are needed, you must always define them in your ref classes. Just to remind you, for a type T in a C++/CLI class, these functions will be of the form:

T(const T% t)                          // Copy constructor
{
  // Function body...
}

 T% operator=(const T% t)               // Assignement operator
{
  // Function body...
}

 ~T()                                   // Destructor
{
  // Function body...
}

Some container operations also require a no-arg constructor and the operator==() function to be defined. A no-arg constructor may be used if space for elements in a container needs to be allocated when the container is storing objects rather than handles. If you are storing handles to reference types or value types in an associative container such as a set or a map, they must overload at least one comparison operator — the default requirement is for operator<().

Using Sequence Containers

All the sequence containers provided by STL for native C++ are also available with the STL/CLR. The STL/CLR implements all the operations that the native STL containers support, so the differences tend to be notational, arising from the use of C++/CLI types. Let's explore the differences through some examples.

Using Associative Containers

All the associative containers in STL/CLR work in essentially the same way as the equivalent native STL containers, but there are some important small differences, generally to do with how pairs are represented.

First, the type of an element that you store in a map of type

map<K, T>

in native STL is of type pair<K, T>, but in an STL/CLR map container it is of type map<K,T>::value_type, which is a value type. This implies that you can no longer use the make_pair() function to create a map entry in STL/CLR. Instead you use the static make_value() function that is defined in the map<K, T> class.

Second, the insert() function that inserts a single element in a map<K, T>, returns a value of type pair<map<K, T>::iterator, bool> for a native STL container, whereas for an STL/CLR map<K, T> container, the insert() function returns an object of type

map<K, T>::pair_iter_bool

which is a reference type. An object of type map<K, T>::pair_iter_bool has two public fields, first and second. first is a handle to an iterator of type

map<K, T>::iterator^

and second is of type bool. If second has the value true, then first points to the newly inserted element; otherwise it points to an element that already exists in the map with the same key.

I'll take one example to illustrate how associative containers in the STL/CLR work, and reproduce Ex10_11 as a C++/CLI program.

LAMBDA EXPRESSIONS IN C++/CLI

You can use lambda expression in your C++/CLI programs and with STL/CLR. There is one restriction, though. You cannot capture C++/CLI managed types, only standard C++ fundamental and class types. You, however, can specify a lambda parameter as a managed type, so you still have the potential for accessing variables of managed types in the enclosing scope from within the body of a lambda. Apart from this one exception, lambda expressions work exactly the same in C++/CLI code as they do with native C++.

SUMMARY

This chapter introduced the capabilities of the STL in native C++ and demonstrated how the same facilities are provided by STL/CLR for use in your C++/CLI programs.

My objective in this chapter was to introduce enough of the details of the STL and STL/CLR to enable you to explore the rest on your own. There's a great deal more there than I've been able to discuss here, so I encourage you to browse the documentation.

WHAT YOU LEARNED IN THIS CHAPTER

TOPIC

CONCEPT

Standard Template Libraries

The STL and STL/CLR capabilities include templates for containers, iterators, algorithms, and function objects.

Containers

A container is a class object for storing and organizing other objects. Sequence containers store objects in a sequence, like an array. Associative containers store elements that are key/object pairs, where the key determines where the pair is stored in the container.

Iterators

Iterators are objects that behave like pointers. Iterators are used in pairs to define a set of objects by a semi-open interval, where the first iterator points to the first object in the series, and the second iterator points to a position one past the last object in the series.

Stream iterators

Stream iterators are iterators that allow you to access or modify the contents of a stream.

Iterator categories

There are four categories of iterators: input and output iterators, forward iterators, bidirectional iterators, and random access iterators. Each successive category of iterator provides more functionality than the previous one; thus, input and output iterators provide the least functionality, and random access iterators provide the most.

Algorithms

Algorithms are template functions that operate on a sequence of objects specified by a pair of iterators.

Function objects

Function objects are objects of a type that overloads the () operator (by implementing the function operator()() in the class). The STL and STL/CLR define a wide range of standard function objects for use with containers and algorithms; you can also write your own classes to define function objects.

Lambda expressions

A lambda expression defines an anonymous function object without the need to explicitly define a class type. You can use a lambda expression as an argument to STL algorithms that expect a function object as an argument.

Polymorphic function wrappers

A polymorphic function wrapper is an instance of the function<> template that you can use to wrap a function object. You can also use a polymorphic function wrapper to wrap a lambda expression.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.223.168.2