A gentle introduction to the standard template library (STL)

Document Sample
scope of work template
							      A gentle introduction to the standard
            template library (STL)



Some references:


http://www-leland.stanford.edu/~iburrell/cpp/stl.html

http://www.cs.rpi.edu/~musser/stl.html


http://www.sgi.com/Technology/STL/ (this is so comprehensive
  that it documents things that aren’t even part of STL yet)
                      STL Organization


containers - hold collections of objects
iterators - iterate through a container’s elements
algorithms - use iterators to implement sort, search, etc.
             Where the STL code is located
Containers:
#include <list>
#include <vector>
#include <map>
Plus more: deque, queue, stack, set, bitset
Iterators (although you don’t need to include this explicitly to use
   iterators):
#include <iterator>

Algorithms:
#include <algorithm>

Utilities:
#include <utility>
#include <functional>
STL was adopted in 1994 (fairly recently), so your compiler may not
  support it yet.

See
  http://www.cyberport.com/~tangent/programming/stl/com
  patibility.html
for a list of compatible compilers.

Try the following:

#include <vector>
#include <list>
using namespace std; // may or may not be necessary

...
                            Containers
Sequences:
• basic sequences: vector, list
• more exotic sequences: deque, queue, stack,
  priority_queue

Associative containers:
• map (usually a binary tree), multimap, set, multiset

But there are no hash tables (sorry...)
vector: usually implemented as a pointer to an array:




• pro: implements fast indexing (looking up the nth element can be
  done in constant time)
• con: inserting elements into the middle of an array requires
  copying many elements to make room for the new element
list: usually implemented as doubly linked lists



• pro: easy to insert and remove elements from middle of list (just
  rearrange pointers, no copying needed)
• con: indexing is slow (finding the nth element requires scanning
  through the list)
        common vector, list constructors

vector<float> v1; // create an empty vector of floats
vector<float> v2(10, 5.0); // create a vector that
                          // initially holds 10 floats,
                          // each initialized to 5.0

list<float> l; // create an empty list of floats
          Adding elements to vectors, lists
Elements can be appended to the back of a vector with
   push_back:

vector<float> v1;
v1.push_back(6.0);
v1.push_back(7.0);
v1.push_back(8.0);


Now v1 contains 3 elements: {6.0, 7.0, 8.0}

Note that the vector is automatically resized so that each new
  element added with push_back will fit (yay!).
The first and last elements of vectors and lists can be retrieved
  with the front() and back() member functions:

vector<float> v1;
v1.push_back(6.01);
v1.push_back(7.01);
v1.push_back(8.01);
v1.push_back(9.01);

float fFront = v1.front();
float fBack = v1.back();
cout << fFront << " " << fBack << endl;


This prints:

6.01 9.01
pop_back() removes the last element from the vector:

vector<float> v1;
v1.push_back(6.01);
v1.push_back(7.01);
v1.push_back(8.01);
v1.push_back(9.01);
v1.pop_back();

cout << v1.front() << " " << v1.back() << endl;


This prints:

6.01 8.01
lists support push_front and pop_front (to insert and
  remove elements at the front of a list), as well as push_back and
  pop_back:

list<float> l; // create an empty list of floats
l.push_back(6.01);
l.push_back(7.01);
l.push_back(8.01);
l.push_front(9.01);
l.pop_back();

cout << l.front() << " " << l.back() << endl;


This prints:

9.01 7.01
vectors support random access with [] and at(). [] performs
  random access with no bounds checking, while at() performs
  bounds checking, and throws an out_of_range exception if the
  index is out of bounds:

vector<float> v2(10, 5.0); // v2 has 10 elements
v2[3] = 5.0;
v2[14] = 6.0; // XXX: out of bounds! memory corruption!
v2.at(14) = 6.0; // out of bounds, throws an exception

cout << v2.at(3) << endl;



Warning: [] and at() do not resize the vector if an index is out
 of bounds! If you need to increase the size of the vector, use
 push_back to add elements.
Both vectors and lists also support:

size() returns the number of elements in the container
empty() returns true if size()==0, and false otherwise
Summary of functions covered so far:

     vector                    list
   ----------------------------------------
   vector()                    list()
   vector(size, init_val)
   push_back                   push_back
                               push_front
   pop_back                    pop_back
                               pop_front
   front                       front
   back                        back
   []
   at
   size                        size
   empty                       empty
So a hypothetical, simplified, definition of vector might look like:

template <class T> class vector {
public:
    vector();
    vector(int n, const T& val);
    void push_back(const T& x);
    void pop_back();
    T& front();
    T& back();
    T& operator[] (int n);
    T& at(int n);
    int size() const;
    bool empty() const;
};


A simplified list class would look similar.
template <class T> class vector {
public:
    vector();
    vector(int n, const T& val);
      vector(const vector<T>& x);
      ~vector();
      vector<T>& operator= (const vector<T>& x);
     ...
};

Of course, vector will have a copy constructor, assignment
  operator, and destructor. The copy constructor and assignment
  operator for STL container classes make copies of the entire
  container. This can be expensive, so if you don’t want a copy, you
  should pass containers by reference:
      void foo(vector<float> &v);
rather than by value:
      void foo(vector<float> v); // copies entire vector
Even if you never make copies of entire containers, your elements
  may be copied as they are inserted into the containers (and at other
  times).
So if the elements are classes with pointers, they should implement
  the correct copy constructor, assignment operator and destructor.
For classes that shouldn’t be copied (such as large classes or classes
  with virtual functions), a container of pointers to objects can be
  used:
vector<Shape*> shapes;

Be aware that the container will not delete the pointed-to objects for
  you:
vector<Shape*> shapes;
shapes.push_back(new Circle());
shapes.pop_back(); // memory leak: the Circle
                   // was not deleted!

Correct:
vector<Shape*> shapes;
shapes.push_back(new Circle());
delete shapes.back();
shapes.pop_back();
For classes that aren’t copiable (such as large classes or classes with
  virtual functions), a container of pointers to objects can be used:
vector<Shape*> shapes;

Also note that containers of pointers to objects are copied by shallow
  copy, not deep copy:
vector<Shape*> shapes1;
shapes1.push_back(new Circle());
vector<Shape*> shapes2 = shapes1; // make copy of
                                  // entire vector


Now both shapes1 and shapes2 point to the same Circle
  object. Be careful not to delete this Circle object twice!
STL allows a container’s memory management to be customized
  with user defined allocators:
template <class T, class A = allocator<T> > class
  vector {
public:
    typedef typename A::size_type size_type;
     vector();
     vector(size_type n, const T& val);
     vector(const vector<T>& x);
     ~vector();
     vector<T>& operator= (const vector<T>& x);
     void push_back(const T& x);
     void pop_back();
     T& front();
     T& back();
     T& operator[] (size_type n);
     T& at(size_type n);
     size_type size() const;
     bool empty() const;
};
template <class T, class A = allocator<T> > class vector {
public:
    typedef typename A::size_type size_type;
    vector();
    vector(size_type n, const T& val);
    ...
};

For instance, an allocator might be written so that objects are stored
  in a large, persistent database. In this case, size_type might be
  larger than a normal int (it might be a 64-bit integer, for instance).
The “= allocator<T>” specifies a default value for vector’s allocator,
  which is used if you don’t pass in an explicit allocator of your
  own. You probably will never have to worry about allocators
  explicitly; chances are you’ll only need the default allocator.
However, if you have an old compiler that doesn’t support default
  template values, you may have to pass in the default allocator
  explicitly (yuck):
vector<float, allocator<float> > v1(10, 5.0);
                             Iterators
      vector                   list
   ----------------------------------------
   vector()                    list()
   vector(size, init_val)
   push_back                   push_back
                               push_front
   pop_back                    pop_back
                               pop_front
   front                       front
   back                        back
   []
   at
   size                        size
   empty                       empty

You may have noticed that list had operations front() and back() to
  read the first and last elements, but how do you get to the rest of
  the elements?
Every container class defines one or more iterators that can be used
  to scan through the elements of the container:
template <class T, class A = allocator<T> > class list {
public:
    typedef typename A::size_type size_type;
    typedef ... iterator;
    typedef ... const_iterator;
    list();
    list(const list<T>& x);
    ~list();
    list<T>& operator= (const list<T>& x);
    void push_back(const T& x);
    void push_front(const T& x);
    void pop_back();
    void pop_front();
    T& front();
    T& back();
    size_type size() const;
    bool empty() const;
};
Example:
list<float> l;
l.push_back(6.01);
l.push_back(7.01);
l.push_back(8.01);
l.push_back(9.01);

// Use a list iterator to iterate through l:

list<float>::iterator i;
for(i = l.begin(); i != l.end(); ++i) {
    cout << *i << " ";
}


This prints:
6.01 7.01 8.01 9.01
list<float>::iterator i;
for(i = l.begin(); i != l.end(); ++i) {
    cout << *i << " ";
}


list<float>::iterator refers to the iterator defined in the
  class list:




template <class T, class A = allocator<T> > class list {
public:
    typedef typename A::size_type size_type;
    typedef ... iterator;
    typedef ... const_iterator;
    list();
    ...
};
list<float>::iterator i;
for(i = l.begin(); i != l.end(); ++i) {
    cout << *i << " ";
}

A list iterator supports several operations:
• The dereference operator (*) returns the element pointed to by the
   iterator
• ++ advances the iterator to the next element
• != compares the iterator with another iterator to see if the end of
   the list has been reached
template <class T, class A = allocator<T> > class list {
public:
    typedef typename A::size_type size_type;
    typedef ... iterator;
    typedef ... const_iterator;
    list();
    ...
};
list<float>::iterator i;
for(i = l.begin(); i != l.end(); ++i) {
    cout << *i << " ";
}


list contains begin() and end() functions which return
  iterators pointing to the beginning and end of the list:

template <class T, class A = allocator<T> > class list {
public:
    typedef typename A::size_type size_type;
    typedef ... iterator;
    typedef ... const_iterator;
      iterator begin();
      const_iterator begin() const;
      iterator end();
      const_iterator end() const;
     ...
};
list<float>::iterator i;
l.push_back(6.0);
l.push_back(7.0);
l.push_back(8.0);
l.push_back(9.0);
for(i = l.begin(); i != l.end(); ++i) {
    cout << *i << " ";
}

• begin() returns an iterator which points to the first element of
  the list
• end() returns an iterator which points one element past the last
  element in the list. This iterator does not point to a valid element
  (end() is sometimes used as a “null” iterator to indicate a non-
  existent element).
     begin()                                       end()

                       6    7     8       9
     begin()                                 end()

                      6    7   8    9

As a second example of begin() and end(), the following prints
  a list in reverse order:
list<float>::iterator i;
l.push_back(6.0);
l.push_back(7.0);
l.push_back(8.0);
l.push_back(9.0);
for(i = l.end(); i != l.begin(); ) {
    --i; // move backwards in the list
    cout << *i << " ";
}
This prints:
9 8 7 6
                      Iterator categories
Not all iterators support the same operations. For instance, suppose
  we wrote a singly-linked list class:

            begin()                                  end()

                           6     7     8     9


It would be impractical to iterate backwards through this list, since
   the links only point forward. So an iterator for singly linked lists
   might support ++, but not --.
Iterators can be grouped into 5 categories, each supporting a different
   set of operations:

• output: ++, * for writing
• input: ++, ==, !=, * and -> for reading
• forward: ++, ==, !=, * and -> for reading and writing
• bidirectional: ++, --, ==, !=, * and -> for reading and
  writing
• random-access: ++, --, +, -, +=, -=, ==, !=, <,
  >, <=, >=, * and -> for reading and writing

Examples:
• a singly-linked list would create forward iterators
• list (which is a doubly-linked list) creates bidirectional iterators
• vector creates random-access iterators
These categories form a natural hierarchy:

      input
                    forward        bidirectional       random-access
      output

One might think that based on this hierarchy, iterators would be
   implemented as classes with appropriate virtual functions and
   overriding.
However, it was felt that virtual functions would be too inefficient
   for something as critical as iteration, so iterators were not
   implemented as classes with virtual functions.
In fact, iterators are not classes (or even types) at all! Instead, each
   category of iterator is just a set of conventions that various iterator
   implementations abide by.
I said in the last lecture that it was bad style to use a template and to
   make many assumptions about the type T passed in:

template<class T> // assumes T supports rotate, draw
void rotateAndDraw(T &s, double angle) {
    s.rotate(angle);
    s.draw();
}


Unfortunately, this is exactly what STL does with iterators.
  Sometimes this can be confusing, but as long as we understand
  what is required of the 5 categories of iterators, we can live with it.
There are a couple more container operations, insert() and
  erase(), based on iterators:

     vector                   list
  ----------------------------------------
  vector()                    list()
  vector(size, init_val)
  push_back                   push_back
                              push_front
  pop_back                    pop_back
                              pop_front
  front                       front
  back                        back
  []
  at
  size                        size
  empty                       empty
  begin, end                  begin, end
  insert                      insert
  erase                       erase
• insert adds a new element immediately before an iterator.
• erase removes the element pointed to by an iterator

Example:

list<float> l;
l.push_back(6.0);
l.push_back(7.0);
l.push_back(8.0);
l.push_back(9.0);


list<float>::iterator i = l.begin();
i++;
l.insert(i, 17.0);
l.erase(i);


This results in the list {6.0, 17.0, 8.0, 9.0}
Warning: the erase operation causes all iterators that pointed to the
 erased element to be invalidated. So the following is illegal:

list<float>::iterator i = l.begin();
i++;
l.insert(i, 17.0);
l.erase(i);
l.insert(i, 18.0); // illegal! i is invalid here
For vectors, any operation that adds or removes elements
  invalidates all the iterators into the vector. This includes
  push_back, pop_back, insert, and erase:

vector<float>::iterator i = v.begin();
i++;
v.push_back(); // this invalidates the iterator i
v.insert(i, 17.0); // illegal! i is invalid here


The reason for this is that the vector’s internal array may be copied
  to a new memory location when the array is resized, meaning that
  any iterators the pointed into the old array are no longer valid.

Also, remember that insertion and deletion are potentially expensive
  operations for vectors (though they are cheap operations for
  lists).

						
Related docs