The STL, containers and iterators

The C++ STL

The C++ library contains many classes which are implemented as templates. These classes are defined in the C++ standard template library - the STL. The STL also contains many common algorithms which are type-independent (eg sort()).

Why should we use the STL?

Container classes and Data Structures

The C++ STL provides many class templates which are designed to hold your data. Classes designed to hold objects are called "containers".

The most basic of these is the vector<> which we've used all semester. Others with which you may not be familiar are Lists and Set/Multiset and Map/Multimap are described below. Each container has its own header file.

All containers support basic functions such as size( ), empty( ) and clear( ).

Vector

A Vector is a dynamic array with elements always in a certain order (ie first, second, third..). Vectors support random access using operator[], and at( ). Vectors also support the concepts of size( ) and capacity( ).

List

A List is a sequential container that manages its elements using a linked-list (and therefore dynamically allocated memory). Therefore List does not support random access using operator[], at( ) as Vector does. The basic functions of List are insert( ), push_back( ), push_front( ), pop_front( ), pop_back( ), and erase( ).

Set and MultiSet

A Set sorts its elements automatically according to a sort criteria (operator< by default). This means that you may not change the value of an element in the Set. Set does not support direct access of its elements ( ie. no operator[] or at( ) ).

A Multiset is a Set that allows duplicates.

The basic functions for sets are insert( ) and erase( ), count( element ), and find( element ).

Pairs

The class pair is used to treat two related values as a single unit and is often used in the STL. A pair is template that has two parameters, the types of the two values. To use the pair class, #include the header <utility>.

For example, to combine an int and a string into a single unit you might make a declaration like

pair<int, string> hello( 5, "Hello"); Since pair is really a struct, you can then access the values in the pair using standard "dot" notation. The names of the values are "first" and "second". cout << hello.second << endl; // prints "hello" To make coding easier, a function template named make_pair( ) can be used to create pair objects. We could have written the declaration above as pair<int, string> hello = make_pair(5, "hello"); but a more common use of make_pair is to create unnamed pairs used as parameters, as we'll see in a minute.

Map and Multimap

Map containers manage key/value pairs as elements. The pairs are sorted according to the key using a sort criterion ("less than" by default). A multimap is a Map that allows duplicates. To use map or multimap, include the header file <map>.

Maps are templates which have two parameters -- the first is the type of the element's key and the second is the type of the element's value. The element (key/value pair) must be assignable (operator=) and copyable. In addition, the key must be comparable using the sort criterion.

Maps do not provide direct element access. The key of each element is considered to be constant. This is necessary to ensure that you don't compromise the order of the pairs by inadvertently changing a key.

The basic functions for a map are insert( ) and erase( ), count( key ), and find( key ).

We'll see example code for maps below.

Iterators

As the user of these containers, we have no idea how the data is stored in the container, nor should we. However, very often we wish to examine the content of the container one element at a time. With vectors we wrote a simple for loop like this for (unsigned int i = 0; i < myVector.size(); i++) { // do something with myVector[i] } Using operator[ ] was familiar to us because vectors are very much like arrays, but this may not be as meaningful or even possible for other containers since there's no guarantee that the data in other containers is store contiguously in memory or that the container supports random access.

The STL provides a common, generic mechanism that allows us to examine each element of any container in the same way. This mechanism is implemented through the "iterator" class. Iterators provide a uniform interface for access to the elements of a container -- you don't have to remember a different interface for each container. All containers support iterators. An iterator is an object that navigates over the elements of a container for us, while hiding the exact structure of the container from us. It provides a general method of accessing the contents of a container "in order" An iterator represents a "position" in the container.

Iterators are created by the container and returned to the user through various container member functions. The most common member functions that return iterators are begin() and end() which are supported by all containers. A pair of iterators is used to represent a range of elements.

begin( ) returns a bidirectional iterator that represents the first element of the container.
end( ) returns an iterator that represents the end of the elements (not the "last" element). The end is a position behind the last element. Defining end() in this way gives us a simple ending criteria for our loops (as we'll see) and it avoids special handling for empty ranges of elements (for an empty range, begin() == end() ).

Let's take a look at an example. Compare the for loop in the code below with the for loop above.

#include <iostream> #include <set> using namespace std; int main ( ) { set<int> iSet; iSet.insert(4); iSet.insert(12); iSet.insert(7); // this looping construct works for all containers set<int>::const_iterator position; for (position = iSet.begin(); position != iSet.end(); ++position) { cout << *position << endl; } return 0; } Here's an example of using a map. #include <iostream> #include <string> #include <map> using namespace std; int main ( ) { // create an empty map using strings // as keys and floats as values map<string, float> stocks; // insert some stock prices stocks.insert( make_pair("IBM", 42.50)); stocks.insert( make_pair("XYZ", 2.50)); stocks.insert( make_pair("C", 142.50)); stocks.insert( make_pair("UMBC", 12.50)); stocks.insert( make_pair("TM", 62.0)); stocks.insert( make_pair("WX", 0.50)); // instantiate an iterator for the map map<string, float>::iterator position; // print all the stocks for (position = stocks.begin(); position != stocks.end(); ++position) cout << "( " << position->first << ", " << position->second << " )\n"; return 0; } Note that the syntax used with "position" looks familiar. Conceptually, many folks think of an iterator as an abstraction of a pointer (it "points" to an element of the container).

The * operator is used to retrieve the data from the container.
The ++operator (increment) moves "forward" to the next data item in the container.
Most iterators also support the --operator (decrement) for moving "backwards" in the container.
Iterators that support both ++ and -- are known as "bidirectional" iterators.
Iterators also support operator==, operator!= for comparison and operator= for assignment.

Iterators and container methods

Iterators were also devised to help provide a common interface for all containers. They are often used as parameters to container methods and STL algorithms.

Some of you have already used the vector's erase( ) method. It's parameter is an iterator. Using erase( ) with other containers works exactly the same way.

#include <iostream> #include <vector> using namespace std; int main ( ) { vector<int> iVect; iVect.push_back( 44 ); iVect.push_back( 17 ); iVect.push_back( 83 ); iVect.push_back( 8 ); iVect.push_back( 12 ); iVect.push_back( 99 ); iVect.push_back( 92 ); // print the vector vector<int>::const_iterator pos; cout << "original vector\n"; for (pos = iVect.begin(); pos != iVect.end(); ++pos) cout << *pos << endl; // erase an element iVect.erase( iVect.begin()); // print the vector again cout << "\nVector after erase()\n"; for (pos = iVect.begin(); pos != iVect.end(); ++pos) cout << *pos << endl; }

Since iterators are created on demand (ie by calling a container member function), we can have as many iterators as we want for the same container. This example shows the use of two iterators for the same vector.

#include <iostream> #include <vector> using namespace std; int main ( ) { vector<int> iVector; iVector.push_back( 44 ); iVector.push_back( 17 ); iVector.push_back( 83 ); iVector.push_back( 8 ); iVector.push_back( 12 ); iVector.push_back( 99 ); iVector.push_back( 92 ); // print the vector vector<int>::const_iterator pos; cout << "original vector\n"; for (pos = iVector.begin(); pos != iVector.end(); ++pos) cout << *pos << endl; // use multiple iterators to find and remove the middle element vector<int>::iterator start, end; start = iVector.begin(); end = iVector.begin() + iVector.size() - 1; while (start < end) { ++start; --end; } cout << "\nRemoving " << *start << endl; iVector.erase(start); // print the vector again cout << "\nVector after erase()\n"; for (pos = iVector.begin(); pos != iVector.end(); ++pos) cout << *pos << endl; } This is just an introduction to iterators. There are different types of iterators, some auxiliary iterator functions, restrictions and caveats when using iterators. See your favorite STL reference book for a more detailed discussion of iterators and containers.


Last Modified: Monday, 28-Aug-2006 10:16:07 EDT