13.4.2. Tree Example 2: Two Template Variables

Review

Containers implemented with a single template variable store information in a single object, represented by the member variable data in the preceding example. That variable includes the key as part of the data. Containers implemented with two template variables separate the key from the stored information, representing them with the member variables key and value in the following examples. Informally, we can represent this relationship as data = key + value (see Visualizing template variables with tables). Crucially, the stored value does not include the key, so the two are not "naturally" associated.

Client programs can build the association by creating a class with both the key and value as member variables. Alternatively, we can move the association-forming variables and operations to the container. If the container is sufficiently general and reusable, we only need to complete this task once. Two-template-variable binary trees provide the necessary flexibility, mapping the key to the value. In most cases, the binary tree search function is relatively fast, making the overall mapping operation fast.

This section presents two mapping examples. The first is similar to the previous example but demonstrates a binary tree class based on two template variables. The second counts the number of unique words in a book. Both examples use a binary tree named KVTree that maps or associates a key with a value (a K-V pair). The class does not allow duplicate keys, and it only allows mapping a key to one value.

The KVTree class

template <class K, class V>
class KVTree
{
    private:
        K               key;
        V               value;
        KVTree<K,V>*    left = nullptr;
        KVTree<K,V>*    right = nullptr;

    public:
        ~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);

    private:
        void remove(KVTree<K,V>* top, KVTree<K,V>* bottom);
        void _list();
        int  subtrees();
};
Two template-variable binary tree class. Modifying the binary tree from a single template variable, T, to two variables, K and V, requires some modest but anticipated changes. The "boilerplate" code highlighted in yellow introduces the template variables. This modification also applies to the member functions. The modifications marked in red reflect the increased number of variables and their new names, which are also reflected in the member functions.
template <class K, class V>
V* KVTree<K, V>::insert(K key, V value)
{
    KVTree<K, V>* top = this;
    KVTree<K, V>* bottom = right;

    while (bottom != nullptr)
    {
        if (bottom->key == key)
            return &bottom->value;

        top = bottom;
        bottom = (top != this && key < bottom->key) ? bottom->left : bottom->right;
    }

    bottom = new KVTree;
    bottom->key = key;
    bottom->value = value;
    ((top != this && key < top->key) ? top->left : top->right) = bottom;

    return &bottom->value;
}
insert and other member functions. The insert function illustrates the modifications necessary for all member functions (yellow and red). Furthermore, it illustrates the crucial difference between the one- and two-variable versions: the highlighted assignment statements. The one-variable version only needed the value assignment operation (green), but this version adds a key assignment (blue), mapping the key to the value.

Two-Template-Variable Employee Example

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

class Employee
{
    private:
        string  name;
        string  address;

    public:
        Employee(string n="", string a = "") : name(n), address(a) {}

        friend ostream& operator<<(ostream& out, Employee& me)
        {
            out << me.name << " " << me.address;
            return out;
        }
};
Modified Employee class. The Employee class modifications are perhaps unexpected. This version no longer needs the overloaded equality and less-than operators because it uses the string class's operators directly. Aside from removing the overloaded operators, no other changes are necessary.
int main()
{
    KVTree<int, Employee>	tree;

    tree.insert(400, Employee("Dilbert", "225 Elm"));
    tree.insert(100, Employee("Alice", "256 N 400 W"));
    tree.insert(800, Employee("Wally", "718 Washington"));
    tree.insert(500, Employee("Asok", "967 E 900 S"));
    tree.insert(700, Employee("Dogbert", "578 Indiana"));
    tree.insert(900, Employee("PHB", "633 Adams"));
    tree.tree_view();
    cout << "Search: " << *tree.search(500) << endl;
    tree.remove(700);
    tree.tree_view();

    return 0;
}
A simple driver validating the KVTree and its functions.
#include <iostream>
#include <string>
#include "KVTree.h"
#include "Employee.h"
using namespace std;

Employee make_employee();

int main()
{
    KVTree<int, Employee>  tree;
    int                    id;

    while (true)
    {
        . . .

        switch (operation)
        {
            case 'N':
            case 'n':
            {
                Employee v = make_employee();
                cout << "Employee ID?: ";
                int id;
                cin >> id;
                tree.insert(id, v);
                break;
            }

            case 'S':
            case 's':
            {
                cout << "Employee ID?: ";
                int id;
                cin >> id;
                Employee* e = tree.search(id);
                if (e != nullptr)
                    cout << "Employee: " << *e << endl;
                else
                    cout << id << " NOT FOUND" << endl;
                break;
            }

            . . .
        }
    }

    return 0;
}
The two-template variable Employee example. The figure abridges Employee.cpp, a client program using the KVTree template class, by replacing some code with ellipses for brevity. The full version is available at the bottom of the page. The highlighted statements illustrate using the KVTree class.

Two-Template-Variable Word Count Example

The word count problem is more authentic than previous examples, and one of the benefits of this increased authenticity is that it tends to push the boundaries of what we know, forcing us to learn easily ignored details. For example, we can write a program to count and print the number of unique words in a book based on the current version of the KVTree. Ironically, we can't associate a specific word with its count! The purpose of the two-template-variable binary tree was disassociating the key and value. Although we could more easily solve this problem with a one-template-variable class (such as Tree described in the previous section), we can use it to introduce two additional C++ constructs: nested classes and iterators.

Nested Classes

C++ allows programmers to specify a class, called the nested class (or inner class in Java), inside another class, called the outer class. Nested classes may access the members of the outer class regardless of their visibility - they can access protected and private members in addition to those that are public. Nested classes are most useful when their operations are tightly bound to the outer class and its members.

class Outer
{
    public:
        class Nested
        {
            ...
        };
};
 
 
class Outer
{
    public:
        class Nested;    // forward declaration
};

Outer::Nested
{
    ...
};
Specifying a nested class. Programmers may specify a nested class completely inside the outer class if it is small. Alternatively, programmers can use a forward declaration to introduce the nested class's name and specify it separately from the outer class, keeping the outer class's specification uncluttered.

Iterators

C++ iterators are objects that sequentially access the elements stored in a container object. Their name derives from their ability to loop, step, or iterate through the container's elements. They are often implemented as nested classes, binding them to a specific container and allowing them to access its private members. We used string iterators in one solution of the number-palindrome problem. We go further in this example by creating a simple iterator to access the keys in a two-template-variable binary tree.

class iterator;

int count(int number = 0);

iterator get_keys()
{
    iterator i(this);
    return i;
}
template <class K, class V>
int KVTree<K,V>::count(int number)
{
    if (left != nullptr)
        number = left->count(number + 1);
    if (right != nullptr)
        number = right->count(number + 1);
    return number;
}
(a)(b)
KVTree members supporting iterators. Iterator objects are tightly bound to a single container object and provide the client with a limited interface. The tight binding and restrictive interface justify implementing iterators with nested classes having access to the container's non-public members. The KVtree class requires a few modest additions to support the iterator.
  1. Supporting iterators requires three additional KVtree members, all added to its public section: a forward reference, a prototype for a new member function, and a function to create and return an iterator. I considered an alternate version of get_keys:
    iterator* get_keys() { return new iterator(this); }
    Although creating the iterator on the heap with new is more compact, memory management is more challenging for the client. This implementation requires the client to explicitly delete the iterator. The final version in (a) makes the iterator an automatic or local client variable, which is automatically deallocated when it goes out of scope. (Note that both versions allocate keys on the heap, and the iterator destructor automatically deallocates it.)
  2. The count function counts the number of nodes in the tree, allowing the iterator constructor to allocate an array with the correct number of elements. We could implement this function as an iterator member, but it represents functionality a client might legitimately use, so we leave it as a KVTree member.
iterator Class iterator Member Functions
template <class K, class V>
class KVTree<K, V>::iterator
{
    private:
        int size = 0;        // number of tree elements
        int index = 0;       // index into keys array
        K* keys = nullptr;   // heap array storing keys

    public:
        // constructors and destructor
        iterator(KVTree<K,V>* outer);
        iterator(iterator& i);
        ~iterator()
            { delete[] keys; }

        // client access functions
        K next()                                    // (a)
            { return keys[index++]; }
        bool has_next()                             // (b)
            { return index < size; }
        void reset()                                // (c)
            { index = 0; }

    private:
        void add_keys(KVTree<K,V>* tree);
};
template<class K, class V>
KVTree<K,V>::iterator::iterator(KVTree<K,V>* outer)       // (d)
{
    size = outer->count();
    keys = new K[size];
    add_keys(outer->right);
    index = 0;
}

template<class K, class V>
KVTree<K,V>::iterator::iterator(iterator& i)              // (e)
{
    size = i.size;
    keys = i.keys;
    i.keys = nullptr;
}

template <class K, class V>
void KVTree<K, V>::iterator::add_keys(KVTree<K,V>* outer) // (f)
{
    if (outer->left != nullptr)
        add_keys(outer->left);
    keys[index++] = outer->key;
    if (outer->right != nullptr)
        add_keys(outer->right);
}
The iterator class and functions. The boilerplate code initiating a template and introducing the variables remains unchanged. However, the class name bound to each function now consists of both the outer and nested class names joined with the scope resolution operator. The iterator class is included with the KVTree class in the KVTree.h header file (source code at the bottom of the page).
  1. Returns the next token in the array as indicated and increments index.
  2. Returns true until the iterator reaches the end of the array.
  3. Resets index to 0, so has_next returns true and next returns the first key.
  4. The iterator constructor's parameter binds it to the out KVTree object, allowing it to count the tree's nodes. The constructor allocates an array for the keys and adds them to the array, resetting index to 0 when finished.
  5. A copy constructor is necessary because the iterator class allocates the keys array on the heap. Setting the parameter keys pointer to null prevents the KVTree destructor from deallocating the keys array, which now "belongs" to the new iterator object. Try drawing a picture of the situation if the purpose of the last step is unclear.
  6. The parameter, outer, points to the KVTree. The function recursively descends the tree, adding the keys from each node to the keys array. The node order in the tree determines the order of the keys in the array.
#include <iostream>		// for console I/O
#include <fstream>		// for fstream
#include <iomanip>		// for setw, left, and right
#include <string>
#include <cctype>		// for tolower
#include "KVTree.h"
using namespace std;

int main()
{
    KVTree<string, int>	tree;					// (a)
    ifstream		file("alice.txt");			// (b)
    int			c;					// (c)
    string		word;					// (d)

    while ((c = file.get()) != EOF)				// (e)
    {
        if (isalpha(c))						// (f)
            word += tolower(c);
        else if (word.length() > 0)				// (g)
        {
            int* count = tree.search(word);			// (h)
            if (count != nullptr)
                (*count)++;					// (i)
            else
                tree.insert(word, 1);				// (j)
            word.clear();					// (k)
        }
    }

    KVTree<string, int>::iterator  keys = tree.get_keys();	// (l)

    while (keys.has_next())					// (m)
    {
        string  word = keys.next();				// (n)
        int     count = *tree.search(word);			// (o)
        cout << left << setw(20) << word <<			// (p)
            right << setw(3) << count << endl;
    }

    return 0;
}
The complete WordCount program. The program counts all unique words in Alice's Adventures in Wonderland, by Lewis Carroll.
  1. Creates a binary tree.
  2. Defines an input file stream object named file that can read the contents of a file named "alice.txt"
  3. A variable holding the characters as the program reads them from the file.
  4. A variable holding a word as the program assembles it from individual file characters.
  5. The get function reads one character from the file and stores it in the variable c. The loop runs until it reaches the end of the file.
  6. Converts alphabetic characters to lowercase.
  7. Treats non-alphabetic characters as word separators. This naive approach fails with contractions and may include non-word character sequences like Roman numerals.
  8. Looks for the word in the tree.
  9. Increments the word's count if it is in the tree.
  10. If it's not in the tree, the function adds it with a count of 1.
  11. Removes all characters from word, preparing it to build the next word in the book.
  12. Creates a KVTree iterator.
  13. Loops through the words in the iterator.
  14. Gets the next word.
  15. Finds the word's count.
  16. Prints the word and its count on the console.

Downloadable Code

ViewDownloadComments
KVTree.h KVTree.h A binary tree class implemented with two template variables - includes the iterator class
Employee.h Employee.h A class replacing the template variable V in the driver example
driver.cpp driver.cpp A driver program for testing and validating the KVTree class
WordCount.cpp WordCount.cpp A program counting the unique words in a book. Uses KVTree and iterator
alice.txt alice.txt The WordCount input file