13.6.1. STL Examples: map, list, pair, and iterator

STL Examples
Time: 00:05:00 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | Slides (PowerPoint)
KVTree With An STL list
Time: 00:03:37 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | Slides (PowerPoint)
Review

We continue the practice in this section of using the word count program to explore containers. In practice, programmers select a container whose behaviors best match a specific problem. However, some examples use containers that are ill-suited to the word count problem in favor of demonstrating STL classes - containers and support classes. Using the STL containers reduces the time and effort necessary to program a problem solution. In the following examples, the basic program logic remains unchanged from the KVTree version: the program reads the input file one character at a time, converts the alphabetic characters to lower case, and appends them to a word. When the word is complete, the program searches a container for the word. If the word isn't in the container, the program adds it with a count of 1; otherwise, it increments the word's count. Please see the earlier example for details omitted for brevity.

STL Iterators

Reiterating (did you see what I did there?) our previous definition, "iterators are objects that sequentially access the elements stored in a container object," allowing a program to loop or iterate through them. The rudimentary iterator we created for the KVTree is quite limited compared with the STL iterators. Although the iterators used by the following programs are all named iterator, their full names, formed with a container class name and the scope resolution operator, are unique - they are not the same iterator.

While programs instantiate the STL iterators from distinct classes, they share several crucial features:

The pair Structure

The pair structure builds an association between two values whose types it represents with template variables. In Chapter 9, I claimed, "The only real difference between a struct and a class is the default visibility: structures have public visibility, and classes have private visibility. Nevertheless, C++ programmers generally only use structures to represent packaged data and reserve classes for truly object-oriented situations (i.e., when an entity should have attributes and operations packaged together)." pair is an exception: programmers often access both fields or members, but giving it functions eases its use.

template <class T1, class T2>
struct pair
{
    T1 first;
    T2 second;
};
The pair structure specification. A pair object associates two general template values. However, the meaning of the values and their association only exists in a problem and the program solving it, making it impossible to derive member names reflecting either the association or the problem. Therefore, the pair structure generically names its members first and second. Several containers transparently use the pair structure, importing the specification themselves, but programmers can use it independently of other containers by including the <utility> header.

WordCount_list1: list, iterator, and Client Structure

Lists are linear data structures whose insert and search operations run in time proportional to n, where n is the number of elements in the list. This proportionality means that doubling the data size doubles the insert and search runtimes, making them ill-suited for the word count problem. Although not an optimal solution, the following example implements WordCount with an STL list. The list-based solution could use the pair class, but a client structure better illuminates the underlying program structure.

#include <iostream>
#include <fstream>
#include <iomanip>
#include <string>
#include <cctype>
#include <list>											// (a)
using namespace std;

struct w_count											// (b)
{
    string	word;
    int		count;
};

int main()
{
    list<w_count>	words;									// (c)
    ifstream		file("alice.txt");
    int			c;
    string		word;

    while ((c = file.get()) != EOF)
    {
        if (isalpha(c))
            word += tolower(c);
        else if (word.length() > 0)
        {
            list<w_count>::iterator i = words.begin();						// (d)
            while (i != words.end() && word > i->word)
                i++;
            
            if (word == i->word)								// (e)
                i->count++;
            else
            {
                w_count node;
                node.word = word;
                node.count = 1;
                words.insert(i, node);
            }

            word.clear();
        }
    }

    for (list<w_count>::iterator i = words.begin(); i != words.end(); i++)	         	// (f)
        cout << left << setw(20) << i->word << right << setw(3) << i->count << endl;

    return 0;
}
An STL list storing a client structure. The STL list class supports limited access to the stored data, only allowing programs direct access to the first and last nodes. Programs must use the list iterator to access the interior nodes, including sequential access. Lists are inherently slower than binary trees, so there is a significant time lapse from the program start to the first output.
  1. The list class specification and function prototypes.
  2. The list container doesn't automatically create a K-V pair associating a word and its count, so the client program creates a structure to make the association.
  3. Instantiates a list container storing w_count objects.
  4. Without a search function, the program must scan the list from the beginning to find the word or its insertion point. It begins by creating a list iterator named i and looping through the words list. The expression word > i->word maintains the list in alphabetical order.
  5. If the word is in the list, increment its count; otherwise, insert it with a count of 1.
  6. The for-loop creates an iterator initialized to the list's first node. The cout statement extracts and prints the word and count from the current. i++ advances the iterator to the next node. Oddly, the end() function returns a "dummy" node one position beyond the list's last node, ending the loop after processing the last node.

WordCount_list2: list, pair, and iterator

#include <iostream>
#include <fstream>
#include <iomanip>
#include <string>
#include <cctype>
#include <list>											// (a)
using namespace std;


int main()
{
    list<pair<string,int>>	words;								// (b)
    ifstream			file("alice.txt");
    int				c;
    string			word;

    while ((c = file.get()) != EOF)
    {
        if (isalpha(c))
            word += tolower(c);
        else if (word.length() > 0)
        {
            list<pair<string,int>>::iterator i = words.begin();
            while (i != words.end() && word > i->first)
                i++;
            
            if (word == i->first)
                i->second++;
            else
                words.insert(i, make_pair(word,1));						// (c)

            word.clear();
        }
    }

    for (list<pair<string,int>>::iterator i = words.begin(); i != words.end(); i++)
        cout << left << setw(20) << i->first << right << setw(3) << i->second << endl;

    return 0;
}
An STL list storing a pair object. This list version is logically identical to the previous example, differing only by replacing the client w_count structure with the STL pair class.
  1. The list and pair class specifications and function prototypes.
  2. Instantiates a list storing instance of pair class, which the program configures to hold a string and int (notice the nest angle brackets).
  3. make_pair, an STL function, creates a pair object, storing word with a count of 1. The insert function inserts the pair in the words list at the location indicated by the iterator i.

WordCount_tree: KVTree, list and iterator

The initial KVTree example included an iterator constructed with an array and implemented as a nested class. The iterator's description suggested replacing the array with a list and, using its iterator, eliminating the nested class. The following example demonstrates this solution.

public:
    class iterator;
    list<K> keys;

    ~KVTree();
    V* insert(K key, V value);
    V* search(K key);
    void remove(K key);

    void list() { if (right != nullptr) right->_list(); }
    void tree_view(int level = -1);
    int count(int number = 0);

    iterator get_keys() { iterator i(this); return i; }
    auto get_keys() { fill_list(); return keys.begin(); }
    auto get_end() { return keys.end(); }

private:
    void fill_list();
    void add_keys(KVTree<K,V>* tree);
    void remove(KVTree<K, V>* top, KVTree<K, V>* bottom);
    void _list();
    int  subtrees();
template<class K, class V>
void KVTree<K,V>::fill_list()
{
    keys.clear();

    if (right != nullptr)
        add_keys(right);
}
(b)
template <class K, class V>
void KVTree<K, V>::add_keys(KVTree<K,V>* tree)
{
    if (tree->left != nullptr)
        add_keys(tree->left);
    keys.push_back(tree->key);
    if (tree->right != nullptr)
        add_keys(tree->right);
}
(a)(c)
KVTree<string, int>::iterator keys = tree.get_keys();

while (keys.has_next())
{
    string  word = keys.next();
    int     count = *tree.search(word);
    cout << left << setw(20) << word <<
        right << setw(3) << count << endl;
}
list<string>::iterator keys = tree.get_keys();
list<string>::iterator end = tree.get_end();
while (keys != end)
{
    string  word = *keys++;
    int     count = *tree.search(word);
    cout << left << setw(20) << word <<
        right << setw(3) << count << endl;
}
(d)(e)
WordCount_tree: KVTree with an STL list and iterator. The example excerpts the illustrated code fragments from the previous version of the KVTree WordCount program. Links to the complete program files are located at the end of the page. Six steps convert KVTree and the associated WordCount.cpp program from the nested, proprietary iterator to an STL list and iterator:
  1. Remove the KVTree::iterator class and its functions from the end of the KVTree header file.
  2. Modify the tree class (a). Remove the crossed-out statements and replace them with those highlighted in blue. This version uses a list of tree keys as the tree iterator. The get_keys function fills the list with the keys and returns a list iterator pointing to the first key. The get_end returns an end iterator the client uses to loop through the keys.
  3. Add the fill_list function (b). The first data node is in the root's right subtree. The tree reuses the list each time the client needs an iterator, so set_fill clears or empties it before refilling it.
  4. Add the add_keys function (c). The function recursively traverses the tree, adding each node's key to the list. The push_back function appends data at the end of the list.
  5. Modify the client program, deleting the crossed out statement (d).
  6. Continue modifying the client, adding the highlighted code (e). The client uses a while-loop and the two iterators to loop through the keys. It searches the tree for each key, printing the word and its count. Compare with a similar for-loop version (f).

Through several variations, the KVTree and its associate WordCount program has demonstrated templates and numerous STL features. The final version doesn't add any new features but integrates features demonstrated above, achieving the example's final improvement. The Figure 4(c) add_keys function only stores the keys in the list forming the tree's iterator. Consequently, the client must search the tree for each key or word (e).

KVTreeWordCount
public:
    list<K> keys;
    list<pair<K,V>> keys;
template <class K, class V>
void KVTree<K, V>::add_keys(KVTree<K,V>* tree)
{
    if (tree->left != nullptr)
        add_keys(tree->left);
    keys.push_back(tree->key);
    keys.push_back(make_pair(tree->key, tree->value));
    if (tree->right != nullptr)
        add_keys(tree->right);
}
list<string>::iterator keys = tree.get_keys();
list<string>::iterator end = tree.get_end();
list<pair<string, int>>::iterator keys = tree.get_keys();
list<pair<string, int>>::iterator end = tree.get_end();
while (keys != end)
{
    string  word = *keys++;
    int     count = *tree.search(word);
    cout << left << setw(20) << word << right <<
        setw(3) << count << endl;
    cout << left << setw(20) << keys->first << right <<
        setw(3) << keys->second << endl;
    keys++;
}
WordCount_tree: KVTree with list, pair, and iterator. The add_keys function visits each tree node while filling the list with keys or words (Figure 4(c)). However, each node also contains the node's value or word count, which, if saved, would eliminate the need for the client to search the tree for each word during output (Figure 4(e)). The figure illustrates the necessary changes - crossing out the deletions and highlighting the additions - to the KVTree class and word count program.

WordCount_map: map and iterator

The STL implements a map with a binary tree, providing fast insert and search operations. Although the operations slow as the tree becomes larger, the degradation is gradual with the runtime generally (ignoring some worst-case scenarios) proportional to log n, where n is the number of tree nodes. In my opinion, the most striking feature of the STL map is its overloading of the index operator, [], to operate as both the insert and search functions. It functions as the search operation on the right-hand side of the assignment operator (i.e., when used as an r-value) and as the insert operation on the left-hand side (i.e., when used as an l-value).

#include <iostream>
#include <fstream>
#include <iomanip>
#include <string>
#include <cctype>
#include <map>											// (a)
using namespace std;

int main()
{
    map<string, int>	words;									// (b)
    ifstream		file("alice.txt");
    int			c;
    string		word;

    while ((c = file.get()) != EOF)
    {
        if (isalpha(c))
            word += tolower(c);
        else if (word.length() > 0)
        {
            words[word]++;									// (c)
            word.clear();
        }
    }

    for (map<string,int>::iterator i = words.begin(); i != words.end(); i++)			// (d)
        cout << left << setw(20) << i->first << right << setw(3) << i->second << endl;		// (e)

    return 0;
}
WordCount_map: STL map solution. A map container's features closely match the word-count problem's requirements, making it the ideal container for a programmed solution. The map is implemented as a binary tree with two template variables, making inserting or finding words fast operations. The template implementation allows programmers to use any appropriate key and value types, specifically string and int in the word-count problem. Finally, the map container's functions and iterators are well-engineered and highly optimized, allowing us to focus on the problem while (mostly) ignoring the container. The map "hides," or at least obscures one significant implementation detail: it automatically stores each key-value pair in an instance of the pair class.
  1. The map and pair class specifications and function prototypes.
  2. Instantiates a map container with string keys and int values.
  3. If we can single out one statement in the program as pivotal, it is this one. If word isn't in the map, words[word] inserts it with a default 0 count. The expression also returns a reference to the value (i.e., the count) associated with word, which ++ increments.
  4. The first for-loop expression defines a map iterator and initializes it to the first map node. The middle expression loops until the iterator has passed the last node. The last expression, i++, increments the iterator to the next node.
  5. The cout statement formats the key and value for output. The iterator, i, points to a pair object, so the program accesses the key (word) and value (count) with the member names first and second.

Downloadable Code

Four versions of the WordCount program count the unique words in a book and display the words and their counts to the console, demonstrating template classes and STL containers.

ViewDownloadComments
WordCount_list1.cpp WordCount_list1.cpp Demonstrates the STL list and iterator classes.
WordCount_list2.cpp WordCount_list2.cpp Demonstrates the STL list, iterator, and pair classes.
KVTree.h KVTree.h A KVTree using a list. Includes pair and non-pair versions.
WordCount_tree.cpp WordCount_tree.cpp
WordCount_map.cpp WordCount_map.cpp Demonstrates the STL map and iterator classes.
alice.txt alice.txt The WordCount input file