Container Classes

Instances of a container class are stores for other types of objects generically called storables. The relationship between a store and a storable is a special type of association called aggregation. A container provides basic operations for inserting, retrieving, and removing storables.

In a heterogeneous container Storable is an abstract base class:

In a homogeneous (or heterogeneous) container Storable can be a template parameter:

Containers differ in how remove(), insert(), and retrieve() are implemented. In a random access container, each stored item is identified by a unique index corresponding to its position in the container. Accessing stored elements is very efficient, but inserting and deleting elements can be expensive. Arrays are random access containers.

In a sequential access container a movable cursor designates a current element. remove() and retrieve() always refer to the current element. Insert() always takes place immediately before or after the current element. Inserting and removing are very efficient, but retrieving involves a sequential search. Files and linked lists are examples of sequential stores.

Locations in an associative access container are designated by unique or semi-unique keys associated with or computed from the storable elements. Retrieving an element is very efficient if the element's key is known. Tables and trees are examples of associative access stores.

Elements are retrieved or removed from a temporal access container according to when they were inserted into the container. LIFO and FIFO stores are examples.

The Standard Template Library

A container is actually a handle that manages a potentially complicated C-style data structure such as an AVL tree, hash table, or doubly linked list (see the Handle-Body idiom). Fortunately, implementing these data structures is largely an academic exercise these days (but a good one). This is because the standard C++ library now comes with a collection of container templates called the standard template library (STL):

Sequences
   vector<Storable>      (random access store)
   list<Storable>         (sequential access store)
   deque<Storable>      (base class store for stacks and queues)
Sequence Adaptors         (temporal access store)
   stack<Storable>      (LIFO store)
   queue<Storable>      (FIFO store)
   priority_queue<Storable> (uses Storable::operator<())
Associative Containers
   map<Key, Storable>   (associative access store)
   multimap<>
   set<>
   multiset<>

Iterators

An iterator is an abstract pointer to an element in a sequence. A sequence can be an array, a container, a stream, or a string. Like pointers, iterators can be incremented, decremented, compared, or dereferenced.

For example, we can initialize an array using index numbers:

int nums[SIZE];
for(int i = 0; i < SIZE; i++)
   nums[i] = 2 * i + 1;

but we could just as easily use a pointer to traverse the array:

int* p;
for(p = nums; p != nums + SIZE; p++)
   cout << *p << ' ';

A pointer into an array is an example of an iterator.

We could do the same for a vector. Here we declare an integer vector, then use index numbers to initialize it:

vector<int> nums(SIZE);
for(int i = 0; i < SIZE; i++)
   nums[i] = 2 * i + 1;

Iterators for vectors may be implemented quite differently from pointers, list, deque, and map iterators (stacks and queues don't have iterators). The iterator type for any container class is given the name iterator inside the class using typedef or inner classes, so declaring an iterator requires qualifying the iterator type name with the container class name:

vector<int>::iterator p;

Initializing an iterator to the beginning of a container, or comparing it to the end of the container requires the container to provide fixed iterators "pointing" to the first element and just beyond the last element. All container classes provide begin() and end() functions for this purpose. The following statement produces the exact same result as the earlier for statement:

for(p = nums.begin(); p != nums.end(); p++)
   cout << *p << ' ';

There are different categories of iterators. Output iterators only allow increment and read operations:

*p = val; p++;

Input iterators only allow increment, compare, and write operations:

val = *p; p++; p == q;

Forward iterators are both input and output iterators. Bidirectional iterators are forward iterators that can also be decremented:

--p;

Random access iterators are bidirectional iterators that can be incremented and decremented by random amounts:

p – 7; q + 6;

Independent of category, an iterator can be declared const or non-const.

The Iterator Design Pattern

STL iterators are instances of the iterator design pattern discussed in Gamma, et al.'s book Design Patterns. They are particularly important in the context of sequential stores such as lists. Sharing a list is difficult because an apparently non destructive operation, such as printing the list, can actually be destructive if it moves the list's internal cursor. An iterator is like an external cursor provided and owned by the client. Modifying an iterator has no effect on iterators owned by other clients of the list.

String & Stream Iterators

Streams and strings are sequences, so iterators can be used with them, too:

void testString(const string& s)
{
   string::iterator p;
   for(p = s.begin(); p != s.end(); p++)
      cout << *p << ' ';
   cout << '\n';
}

 

void testStream(const string& fname)
{
   ifstream is(fname);
   vector<string> v;
   istream_iterator<string> p(is);
   istream_iterator<string> eos;
   for( ;p != eos; p++)
      cout << *p << '\n';
}

STL Containers

Here are some of the header files you need to include if you want to use STL:

#include <vector>
#include <list>
#include <deque>
#include <stack>
#include <queue>
#include <map>
#include <algorithm>
#include <functional>
using namespace std;

Basic Container Operations

Assume c is a container of type yyy<xxx>. Assume x is of type xxx and p is of type yyy::iterator. Here are the most common operations:

c.begin()          = iterator "pointing" to first element of c
c.end()             = iterator "pointing" to one past last element
c.front()          = first element of c
c.back()          = last element of c
c.size()            = number of elements in c
c.empty()         = true if container is empty, false otherwise

 

c.push_back(x)      inserts x at end of c
c.push_front(x)   inserts x at beginning of c
c.insert(p, x)      inserts x in c before *p

 

c.pop_front()      removes first element of c
c.pop_back()      removes last element of c
c.erase(p)         removes *p

Sequences

Vectors

Vectors encapsulate heap arrays. They automatically grow to accomodate any number of elements. The elements in a vector can be accessed and modified using iterators or indeces.

To begin, let's provide a template for printing vectors. We use a constant iterator:

template <class Storable>
ostream& operator<<(ostream& os, const vector<Storable>& v)
{
   os << "( ";
   vector<Storable>::const_iterator p;
   for( p = v.begin(); p != v.end(); p++)
      os << *p << ' ';
   os << ')';
   return os;
}

The next function demonstrates the basic vector operations:

void testVector()
{
   // declaring a couple of vectors
   vector<int> vec1, vec2;

   // adding elements to the end of a vector
   vec1.push_back(7);
   vec1.push_back(42);
   vec1.push_back(19);
   vec1.push_back(100);
   vec1.push_back(-13);

   // traversing a vector using indices
   int n = vec1.size();
   for(int i = 0; i < n; i++)
      vec1[i] = vec1[i] * 2;   // [] is unsafe!
   cout << vec1 << '\n';

   // safe access:
   try
   {
      cout << vec1.at(500); // oops!
   }
   catch(out_of_range e)
   {
      cerr << e.what() << '\n';
   }

   // insertion:
   vector<int>::iterator p;
   p = vec1.begin();
   p = p + 2;   // p a random access iterator
   vec1.insert(p, 777);
   cout << vec1 << '\n';

   // removal:
   p = vec1.begin();
   p = p + 1;
   vec1.erase(p);
   vec1.pop_back();
   cout << vec1 << '\n';
}

Here's the output produced by this function:

( 14 84 38 200 -26 )
invalid vector<T> subscript
( 14 84 777 38 200 -26 )
( 14 777 38 200 )

Exercise

Implement and test a reusable vector reader:

template <class Storable>
istream& operator>>(istream& is, vector<Storable>& v) { ??? }

Users must enter vectors as sequences seperated by white space and bracketed by parenthesis. Should you use a constant or variable iterator?

Warning

I have found some of the STL containers in VC++ assume the members of the Storable class can be compared using == and <. Thus, vector<Employee> isn't allowed (unless Employee::operator==() and Employee::operator<() have been defined), but vector<Employee*> is okay. A related bug seems to prevent storing of the standard complex numbers, as in stack<complex<double> >.

Lists

As before, we begin with a list writer. Only two changes are needed from the vector writer:

template <class Storable>
ostream& operator<<(ostream& os, const list<Storable>& v)
{
   os << "( ";
   list<Storable>::const_iterator p;
   for( p = v.begin(); p != v.end(); p++)
      os << *p << ' ';
   os << ")";
   return os;
}

The test function shows some of the differences between lists and vectors.

void testList()
{
   // declaring a couple of lists
   list<string> vals1, vals2;

   // adding elements to the end of a list
   vals1.push_back("golf");
   vals1.push_back("judo");
   vals1.push_back("pool");
   vals1.push_back("chess");
   vals1.push_back("boxing");

   // adding elements to the front of a list
   vals2.push_front("car");
   vals2.push_front("boat");
   vals2.push_front("plane");
   vals2.push_front("horse");

   cout << vals1 << '\n';
   vals2.reverse(); // for fun
   cout << vals2 << '\n';

   list<string>::iterator p;
   p = vals1.begin();
   p++; p++; // p + 2 not allowed!
   vals1.insert(p, "yoga");
   vals1.remove("golf");
   vals1.sort();
   p = vals2.begin();
   p++;
   vals2.erase(p);
   vals1.merge(vals2); // this is weird
   cout << vals1 << '\n';
}

Here's the program output:

( golf judo pool chess boxing )
( car boat plane horse )
( boxing car chess judo plane horse pool yoga )

Deques

By themselves, deques (rhymes with "checks") are relatively useless. They are optimized for inserting and removing elements at either end, hence are ideal adaptees for stacks and queues.

Maps

A map contains pairs (a standard C++ library type):

template <class KEY, class VALUE>
struct pair
{
   KEY first;
   VALUE second;
};

Here are template functions for writing pairs and maps:

template <class Key, class Value>
ostream& operator<<(ostream& os, const pair<Key, Value>& p)
{
   os << '(' << p.first << ", " << p.second << ')';
   return os;
}

 

template <class Key, class Value>
ostream& operator<<(ostream& os, const map<Key, Value>& m)
{
   map<Key, Value>::const_iterator p;
   for( p = m.begin(); p != m.end(); p++)
      os << *p << '\n';
   return os;
}

 

If m is a map and k is a key, then m.find(k) returns an iterator pointing to the first pair in m having key == k, otherwise m.end() is returned. Here's a template function that can be used to find the value associated with a particular key:

template <class Key, class Value>
bool find(Key k, Value& v, const map<Key, Value>& m)
{
   map<Key, Value>::iterator p;
   p = m.find(k);
   if (p == m.end())
      return false;
   else
   {
      v = (*p).second;
      return true;
   }
}

Here's a sample program:

void testMap()
{
   map<string, int> m;
   m["zero"] = 0;    m["one"] = 1;    m["two"] = 2;
   m["three"] = 3;    m["four"] = 4;    m["five"] = 5;
   m["six"] = 6;       m["seven"] = 7;    m["eight"] = 8;
   m["nine"] = 9;
   cout << m << '\n';
   int n; // storage for find result
   if (find(string("two"), n, m))
      cout << "m[\"two\"] = " << n << '\n';
   else
      cout << "item not found\n";
   if (find(string("too"), n, m))
      cout << "m[\"two\"] = " << n << '\n';
   else
      cout << "item not found\n";
}

Unfortunately, VC++ couldn't figure out if the Key parameter in the find template should be a string or a char*, so I had to perform a manual cast. Here's the program's output. Notice that the entries in a map are automatically sorted by key:

(eight, 8)
(five, 5)
(four, 4)
(nine, 9)
(one, 1)
(seven, 7)
(six, 6)
(three, 3)
(two, 2)
(zero, 0)

m["two"] = 2
item not found

We can use m[k] to search a map m for a key k instead of the find template defined above. However, if the search fails, a "null" value will be associated with k and inserted into m. For objects, the null value is the value constructed by the default constructor. For numbers and pointers, the null value is 0. Sometimes we can use this to our advantage. The following short program counts the number of occurrences of each word in a given file, then displays the result:

#include <map>
#include <iostream>
#include <fstream>
#include <string>
using namespace std;

 

int main(int argc, char* argv[])
{
   if (argc < 2)
   {
      cerr << "usage: " << argv[0] << " <FILE NAME>\n";
      return 1;
   }
   
   ifstream f(argv[1]);
   string s;
   map<string, int> table;
   while (f >> s)
      table[s]++;
   cout << table << '\n';

   return 0;
}

Adapters

The adapter design pattern allows us to change the interface of a class without changing its implementation or functionality. This can either be done using delegation or private inheritance:

Stacks, queues, and priority queues are adapters for sequences. The delegation implementation is used. Both queue and stack provides a push, pop, top interfaces to a deque. Of course queue<>::push() means enqueue() and queue<>::pop() means dequeue(). For example:

template <class T, class C = deque<T> >
class stack
{
protected:
   C c; // adaptee
public:
   typedef C container_type;
   void pop() { c.pop_back(); }
   void push(const T& s) { c.push_back(s); }
   T& top() { return c.back(); }
   // etc.
};

Printing and traversing stacks and queues can be difficult because the underlying deque, always called c, is a protected member. The only way around this is to subclass stack<Item>, so c becomes visible. We can call our derived class stack<>, too, if we leave the STL stack<> and deque<> names in the std namespace. Here are the includes and using declarations we need:

#include <deque>
#include <stack>
#include <iostream>
using std::ostream;
using std::cout;

A writer for deques is needed:

template <class Storable>
ostream& operator<<(ostream& os, const std::deque<Storable>& v)
{
   os << "( ";
   std::deque<Storable>::const_iterator p;
   for( p = v.begin(); p != v.end(); p++)
      os << *p << ' ';
   os << ")";
   return os;
}

Our stack template:

template <class Storable>
class stack: public std::stack<Storable>
{
public:
   std::deque<Storable> getContainer() const { return c; }
};

Finally, a writer for stacks:

template <class Storable>
ostream& operator<<(ostream& os, const stack<Storable>& s)
{
   os << s.getContainer();
   return os;

}

We assume the adaptee type is the default deque<Storable> because VC++ doesn't provide a name for it. A more standard compiler should allow:

template <class Storable>
class stack: public std::stack<Storable>
{
public:
   container_type getContainer() const { return c; }
};