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
Two-Template-Variable Employee Example
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.
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.)
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).
Returns the next token in the array as indicated and increments index.
Returns true until the iterator reaches the end of the array.
Resets index to 0, so has_next returns true and next returns the first key.
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.
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.
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.