An Introduction to the Standard Template Library (STL)Carlos Moreno |
| Copyright © 1999-2002
by Carlos Moreno and Mochima Software/AudioSystems |
IntroductionThe Standard Library is a fundamental part of the C++ Standard. It provides C++ programmers with a comprehensive set of efficiently implemented tools and facilities that can be used for most types of applications. In this article, I present an introduction/tutorial on the Standard Template Library, which is the most important section of the Standard Library. I briefly present the fundamental concepts in the STL, showing code examples to help you understand these concepts. The article requires and assumes previous knowledge of the basic language features of the C++, in particular, templates (both function templates and class templates). For a detailed description or listing of all the STL facilities, you should consult a reference material, such as Bjarne Stroustrup's The C++ Programming Language, 3rd edition , or Matthew Austern's Generic Programming and the STL.
|
| vector | Array |
| list | Doubly-linked list |
| slist | Singly-linked list |
| queue | FIFO (first-in, first-out) structure |
| deque | Array-like structure, with efficient insertion and removal at both ends |
| set | Set of unique elements |
| stack | LIFO (last in, first out) structure |
The definition of these containers and their iterators are provided in the std namespace, and require to #include the appropriate file, which is always the same name of the container. The examples below show how to use specific containers:
#include <vector>
std::vector<double> values;
#include <queue>
using namespace std;
queue<Order> back_orders;
#include <stack>
using namespace std;
stack<Application_descriptor> undo_list;
Of course, don't be misled by the fact that all the containers are manipulated in the same manner: this only makes them look equivalent in the code, but their functionality and performance are obviously different.
Linked lists provide excellent performance for insertions and removals of elements - unlike vectors. However, linked lists require an important overhead for the storage, since they require extra pointers per element, besides the fact that each element requires a single memory allocation. Also, linked lists don't provide direct (random) access to arbitrary elements (i.e., subscripting). This disallows, for instance, the possibility of doing binary search in a linked list. (which we could do on a vector). On the other hand, a vector, which efficiently uses storage space to place elements in consecutive locations, is extremely inefficient if we need to do insertions or removals of elements in the sequence.
And speaking of insertions and removals of elements, I should mention that the idea of manipulating containers in a regular manner goes beyond the concept of the iterator and its related operators.
In particular, most containers provide member-functions to insert
or remove elements from the ends. The "front" operations refer
to operations performed at the beginning (first element), and the
"back" operations at the end (last element). The following member-functions
are provided for most containers:
| push_front | Inserts element before the first (not available for vector) |
| pop_front | Removes the first element (not available for vector) |
| push_back | Inserts element after the last |
| pop_back | Removes the last element |
Also, most containers provide the following member-functions:
| empty | Boolean indicating if the container is empty |
| size | Returns the number of elements |
| insert | Inserts an element at a particular position |
| erase | Removes an element at a particular position |
| clear | Removes all the elements |
| resize | Resizes the container |
| front | Returns a reference to the first element |
| back | Returns a reference to the last element |
The vector and
deque containers provide subscripting access to elements,
and subscripting access with bounds-checking:
| [] | Subscripting access without bounds checking |
| at | Subscripting access with bounds checking |
(there are many other operations provided in the STL, but I will omit a detailed list, given that this article is only an introduction to the STL. Consult a reference book for more details).
Again, don't be misled by the fact that these functions are supported by most containers and produce the same results: the performance will be totally different depending on the specific type of container used: for instance, inserting one element in an array is extremely expensive in terms of execution time (we need to move all the other elements to open up space for the element to insert). With a linked list, however, it takes a constant, small amount of time, regardless the number of elements in the list.
I will now present two examples to show the use of the vector and list containers. These examples indirectly illustrate the generic use of the STL containers.
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int main()
{
vector<string> names;
while (more_data())
{
string temp = get_more_data();
names.push_back(temp);
}
// Maybe a little inefficient
because of the insertions.
// If we know in advance how many
elements we will have,
// we could do:
names.resize (num_elements);
for (int i = 0; i < num_elements; i++)
{
names.at(i) = get_more_data();
}
// We could also use names[i]
instead of names.at(i),
// given that we know for sure
that there won't be a
// subscript overflow in that
for loop, therefore, we
// don't need checked access to
the elements of the array
// Sort the elements -- assuming that we
provided a function to
// do so, say,
// template <class T>
// void sort (vector<T> &);
sort(names);
// Now print the values
for (int i = 0; i < names.size(); i++)
{
cout << names[i] <<
endl;
}
// We could also do it using an iterator
vector<string>::iterator i;
for (i = names.begin(); i != names.end();
++i)
{
cout << *i <<
endl;
}
return 0;
}
#include <iostream>
#include <list>
#include <string>
using namespace std;
class Student
{
public:
// ... various functions to perform the required
operations
private:
string name, ID;
int mark;
};
int main()
{
list<Student> students;
// Read from data base
while (more_students())
{
Student temp;
temp.read();
students.push_back
(temp);
}
// Now print the students that failed
(mark < 60%) - of
// course, the particular Student object
should provide a
// member-function (say, passed()) that
will determine that
list<Student>::iterator i;
for (i = students.begin(); i != students.end();
++i)
{
if (! i->passed()) //
iterators also provide operator ->
{
cout <<
"The student " << *i << " failed." << endl;
//
provided that class Student provides the overloaded
//
stream insertion operator <<
}
}
// Now remove the failed students (of course,
this could have
// been done in the previous loop)
i = students.begin()
while (i != students.end())
{
if (! i->passed())
{
i = students.erase
(i);
}
else
{
++i;
}
}
// ...
return 0;
}
In the removal operation, we must be careful to assign i with the value returned by the member-function erase (it returns an iterator to the element after to the one that was removed). If not, i will be "pointing" to an element that no longer exists (its memory space was just released), therefore, the result of the next i ++ operation would be undefined.
Iterators have, generally speaking, the same advantages and power as the pointers. But of course, they also have the same risks and inconveniences. With a pointer, we can accidentally modify data that we are not supposed to.
With a conventional pointer, we can partially solve that problem by having a pointer point to a data type with the const modifier.
In the first example with arrays, we could code the following:
const int * current;
// ...
And then, if we accidentally try to assign something to *current , or perform any operation that will or might modify the data pointed to by current , the compiler will report an error and stop compiling.
The equivalent idea is provided in the STL with the const iterators. As we have a class iterator defined inside the container class definition, we also have a definition for a class const_iterator , which provides basically the same functionality as a regular iterator , except that modifying the data "pointed to" by the const_iterator is disallowed.
Thus, we could use const_iterators to implement the examples previously shown, as illustrated below:
list<int>::const_iterator current = values.begin();
int high = *current++;
while (current != values.end())
{
if (*current > high)
{
high = *current;
}
++current;
}
In this example, if you accidentally write something like
*current = high;
the compiler will report an error, given that modifying the "dereferenced" value of a const_iterator is disallowed.
How is this implemented? It is indeed simpler than we might think. We already agreed that the iterator class definition provides the unary * operator to provide the "dereferencing" operation (access the value pointed to).
In the case of the iterator for a linked list, we could figure that the * operator's implementation is something along these lines:
T & list<T>::iterator::operator* () const
{
return current->data;
}
Where T is the type in the template definition, and data is, of course, an item of type T .
Notice that the function must return a reference to the data item, such that the return value of the function can be used either as an Lvalue or as an Rvalue.
The equivalent operation, for a const_iterator , would be implemented as:
const T & list<T>::const_iterator::operator* () const
{
return current->data;
}
The only difference is that the function now returns a reference to a constant value, which will prevent client code from modifying the returned value.
There is obviously more: the const_iterator class provides a constructor that takes an iterator of the same container as parameter. The converse is obviously not true: a const_iterator can not be used to create a regular iterator (that would compromise the constness of the object pointed to by the const_iterator ).
As far as the containers are concerned, the functions begin() and end() (which return iterator s) have multiple versions (overloaded member-functions) in which the overloading resolution is done based on the constness of the object that calls the member-function. In particular, the prototypes for the begin() and end() functions would be something along these lines: (the example is shown for a list container)
// ... inside the class list<T> definition:
list<T>::iterator begin();
list<T>::iterator end();
list<T>::const_iterator begin() const;
list<T>::const_iterator end() const;
Thus, if a container object with the const attribute is used to call the functions begin() or end(), the compiler will use the const versions, which return a const_iterator ; if a non-const object is used, the non-const functions will be chosen by the compiler, which return regular iterators.
These are other types of iterators, but I will omit here a detailed
description of the others. Consult a reference book to further
investigate about iterators.