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: PDF, PPTX
KVTree With An STL list
Time: 00:03:37 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | Slides: PDF, PPTX
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, instead favoring the demonstration of STL classes, including containers and support classes. Using STL containers reduces the time and effort required to program a solution to a problem. 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 for the word in a container. 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 within a problem and the program solving it, making it impossible to derive member names that reflect 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 demonstrates how to implement WordCount using 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 provides limited access to the stored data, allowing programs direct access only 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 that 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 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 the features demonstrated above, achieving the example's final improvement. The add_keys function in Figure 4 (c) only stores the keys in the list that forms 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 insertion or retrieval of 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.

STL Functions And Containers With Comparators

The previous examples demonstrated STL containers and functions storing and operating on relatively simple data. The final examples revisit the Employee class and its functors to demonstrate how the STL works with complex objects. The examples organize objects based on their contents, requiring additional support from the client or application program. Programmers provide that support in the form of specialized functors called comparators. The examples reuse the Employee class and its two comparator functions with minor modifications noted in the figures, so they only present the application or main function. The omitted code is described in the first presentation and included with the downloadable code at the bottom of the page.

ApplicationOutput
int main()
{
    vector<Employee> employees;								// (a)

    employees.push_back(Employee("Dilbert", ENGINEER, 400));
    employees.push_back(Employee("Alice", ENGINEER, 100));
    employees.push_back(Employee("Wally", ENGINEER, 200));
    employees.push_back(Employee("Asok", ENGINEER, 700));
    employees.push_back(Employee("PHB", MANAGER, 600));
    employees.push_back(Employee("Richard", MANAGER, 500));
    employees.push_back(Employee("Catbert", OFFICER, 300));

    sort(employees.begin(), employees.end(), compByName());				// (b)

    for (int i = 0; i < employees.size(); i++)
        cout << employees[i] << endl;
    cout << endl;

    sort(employees.begin(), employees.end(), compByNumber());				// (c)

    for (vector<Employee>::iterator i = employees.begin(); i != employees.end(); i++)
        cout << *i << endl;

    return 0;
}
Alice     2  100
Asok      2  700
Catbert   0  300
Dilbert   2  400
PHB       1  600
Richard   1  500
Wally     2  200

Catbert   0  300
Richard   1  500
PHB       1  600
Alice     2  100
Wally     2  200
Dilbert   2  400
Asok      2  700
Sorting a vector with sort using comparators. When first presented in Chapter 11, the comparator example identified the "smallest" of two Employee objects, using a comparator to determine their relative sizes. The STL sort function extends the earlier process by iteratively comparing and rearranging objects until they are in a sorted order, with different comparators achieving different orderings. This example adds #include <vector> to the Chapter 11 version.
  1. The application creates a vector storing Employee object and fills it with example objects.
  2. The STL sort function alphabetizes the objects by the employee's name using the compByName comparator, as illustrated by the first (pink) group of employees in the output.
  3. The second example demonstrates a more complex ordering determined by the compByNumber comparator: officers before managers, managers before engineers, and by ID within position groups, as illustrated by the second (blue) group in the output.

 

int main()
{
    map<Employee, Employee, compByName> employees;								// (a)
    //map<Employee, Employee, compByNumber> employees;								// (b)

    Employee e1("Dilbert", ENGINEER, 400);									// (c)
    Employee e2("Alice", ENGINEER, 100);
    Employee e3("Wally", ENGINEER, 200);
    Employee e4("Asok", ENGINEER, 700);
    Employee e5("PHB", MANAGER, 600);
    Employee e6("Richard", MANAGER, 500);
    Employee e7("Catbert", OFFICER, 300);

    employees[e1] = e5;												// (d)
    employees[e2] = e5;
    employees[e3] = e6;
    employees[e4] = e6;
    employees[e5] = e7;
    employees[e5] = e7;
    employees[e7] = e7;

    for (map<Employee, Employee>::iterator i = employees.begin(); i != employees.end(); i++)
        cout << i->first << "Reports to: " << i->second << endl;

    return 0;
}
Alice     2  100  Reports to: PHB       1  600
Asok      2  700  Reports to: Richard   1  500
Catbert   0  300  Reports to: Catbert   0  300
Dilbert   2  400  Reports to: PHB       1  600
PHB       1  600  Reports to: Catbert   0  300
PHB       1  600  Reports to: Catbert   0  300
Wally     2  200  Reports to: Richard   1  500

Catbert   0  300  Reports to: Catbert   0  300
PHB       1  600  Reports to: Catbert   0  300
PHB       1  600  Reports to: Catbert   0  300
Alice     2  100  Reports to: PHB       1  600
Wally     2  200  Reports to: Richard   1  500
Dilbert   2  400  Reports to: PHB       1  600
Asok      2  700  Reports to: Richard   1  500
Building a map organized with a comparator. STL maps are ordered containers implemented with binary trees. The final example presents two versions of a map ordered with different comparators: the first is arranged alphabetically by the employee's name, and the second by a combination of position and ID number. This example replaces <vector> with <map> and adds a default constructor, Employee() {}, to the Employee class.
  1. Builds a map ordered alphabetically by name.
  2. Builds a map grouping employees with like positions and ordering each group by ID number.
  3. Creates Employee objects for the demonstration.
  4. Maps employees to their immediate supervisor.

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
Employee2.cpp Employee2.cpp Demonstrates the STL sort function used in conjunction with comparators
Employee3.cpp Employee3.cpp Demonstrates the STL map container organized with comparators