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 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 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
#include
using namespace std;
int main ( )
{
set iSet;
iSet.insert(4);
iSet.insert(12);
iSet.insert(7);
// this looping construct works for all containers
set::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
#include
#include
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
#include
using namespace std;
int main ( )
{
vector 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::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
#include
using namespace std;
int main ( )
{
vector 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::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::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