The STL has six kinds of components:
containers
iterators
generic algorithms
function objects
adaptors
allocators.
All of these are provided via C++ templates. Containers are such things
as expandable arrays, called vectors, lists, and sets. Ordinary arrays
of C++ act like containers. The STL also provides Deques, maps
(dictionaries), multi-sets, and multi-maps. Lists are necessarily
doubly linked, but very little is specified about the actual
implementations. Sets, multi-sets, maps, and multi-maps are always kept
sorted.
Iterators generalize C++ pointers and provide a means of manipulating
containers. Unlike other "object-oriented" libraries, in which the
algorithms are provided within the classes of things they manipulate, in
the STL the algorithms are nearly all provided externally to the
container classes. Iterators are the interface between the containers
and the algorithms that manipulate them. This permits an extremely high
degree of reuse as the same sort algorithm will sort ordinary C++ arrays
and STL vectors, among other things.
Function objects are used to tailor some algorithms to special needs. A
function object is anything that supports operator(). This actually
includes ordinary functions, but also objects whose classes define this
operator. For example, the sort algorithm mentioned above uses
operator< by default when comparing objects. This is, of course, not
appropriate when comparing ordinary (char*) strings. One may therefore
provide a function object that provides this "less than" comparison in
operator(). Such a function object may be passed to sort and will be
used in place of operator<. A function object may also be used to
reverse the sort order.
Adaptors adapt other components to special purposes. For example, the
stack container adaptor will "adapt" a vector, list, or deque so that
its interface is that of a stack. There are also iterator adaptors that
make iterators behave differently, by iterating backwards, for example.
Allocators encapsulate a memory model. This is useful in machines (IBM PC's) that have several memory models available. They decouple the algorithms from assumptions about a particular model.
The good news about the STL is that it provides a lot of functionality,
with very good performance at low run-time space cost. Generalized
algorithms are only provided when their efficiency is good. In other
cases a specialized algorithm is provided as well. An example here is
sorting. The generalized sort algorithm could have been adapted to work
on lists, but its efficiency would have been poor. Instead, a special
list sort that is optimized for sorting lists is also provided . Care
has also been taken in the design so that the problem of "code bloat" is
avoided. Separation of algorithms from containers means that
instantiations of the templates result in relatively little added code
and generally only that which is actually going to be used.
The bad news is that the STL provides few programmer comforts. In
particular, programming with the STL does not increase the safety of
programs (other than from the fact that the STL code is well documented
and tested). Most of the interfacing is done with iterators. Iterators
have all of the efficiency and all of the drawbacks of pointers, which
they generalize. The programmer needs to take all necessary care with
subscript bounds, dangling references, deallocation issues, etc. The
vector class supports operator[] and subscripts are NOT checked. As is
usual in C and C++ safety/efficiency tradeoffs are always resolved in
favor of efficiency. This is ameliorated by the fact that the situation
is no worse with iterators than it is with pointers, and the same
techniques that you have learned for pointers apply to iterators. If
you are thoroughly familiar with the relationship between arrays and
pointers in C++, including the pointer duality law, then you should have
no trouble in successfully programming with the STL.
The designers of the STL (Alexaner Stepanov and David Musser, primarily)
took great care so that the algorithms work in many situations. For
example the algorithms work on containers containing constants as well
as on updatable values. There is also a novel mechanism for passing
type information between algorithms so that local variables can be
allocated within the algorithms as needed. This is at the
implementation level, however, and so doesn't get in the way of
learning, unless you try to learn by reading the code itself.
The degree of interaction between the components of the STL is quite
amazing, resulting in great power, but a somewhat daunting learning
task. For example, there are five kinds of iterators ( input, output,
forward, bi-directional, and random access ) plus variations such as
reverse iterators and iostream iterators. Iterators are not a type,
however. There is no iterator class from which other iterators are
derived. An object or other value is an iterator if it possesses an
iterator interface. For example operator++ is required for a forward
iterator (along with other requirements). Bi-directional iterators also
support operator--. Random access iterators support these and operator+
and operator- as well, permitting "iterator arithmetic." The advantage
of this approach is that ordinary pointers are automatically random
access iterators, which means that all of the algorithms of the STL work
with ordinary arrays as well as the containers specifically provided by
the STL itself. The design also means that iterators work with for
loops in the same way that pointers do.
Iterators are incorporated into algorithms via the template mechanism.
The sort routine, for example is defined by a function template whose
argument is a random access iterator. Each type of container exports an
iterator type for use with the algorithms. For example, lists provide
bi-directional iterators, and vectors (expandable arrays) provide random
access iterators. Each container also provides standard members for
obtaining an iterator to its beginning and one to its end. By repeatedly
applying operator++ to the begin() iterator you will eventually reach
the end() iterator. Specifically, the end iterator refers to a "end
location" after the last element in the container, and not to the last
element of the container. These two iterators (begin and end) are
defined even for containers that do not have a linear structure, such as
sets.
The implementation of the containers and algorithms of the STL is not
specified in the standard, but the efficiency of each algorithm is
specified. This efficiency is somewhat contingent, however, on the user
playing by the rules. For example, all function objects are supposed
to execute operator() in constant time. If they do, then sort will have
order N(log(N)) running time. Some algorithms specify one degree of
efficiency in all cases and a better degree if additional work space can
be assured. Many of the running time efficiencies are stated as average
or amortized. For example insertions at the end of a vector (which
might result in its growing in size) are amortized constant time, though
an individual such insertion might be linear in the current size of the
vector.
A very simple example of the use of the STL follows.
vector<int> vec;
int val;
while ( cin >> val)
vec.push_back(val);
// push_back inserts at the end of the vector.
sort(vec.begin(), vec.end());
for(vector<int>::iterator i = vec.begin(); i != vec.end(); ++i)
cout << vec[i];
If we desire that the sort be in decreasing order we can use the
following instead.
bool greater(int a, int b){ return a > b; }
. . .
sort(vec.begin(), vec.end(), greater );
Notice that the iterator type is abstract here and is exported from the
vector<int> class. It acts just like a pointer. The ordinary function
greater will be taken as a function object. The generality of passing
functions or objects implementing operator() interchangeably is achieved
by making the type of the third argument of sort a template parameter.
This means that it is typed by its functionality, not by its place in a
particular type hierarchy.
By Joseph Bergin