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.
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(); };
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 = (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; }
#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; } };
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(800); tree.tree_view(); return 0; }
#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 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.
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 }; class Outer::Nested { ... }; |
private
. Programmers may specify a nested class completely inside the outer class if it is small. Alternatively, they 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. Either way, the outer class's name and the scope resolution operator become part of the nested class's name. Putting the nested class's specification or declaration in a private
section "hides" it from the rest of the program - a helpful organization if the nested class helps the outer by sharing some of its responsibilities. Making the nested class public
, the case with iterators, allows client programs to use the nested class directly.
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) |
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.)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]; if (outer->right != nullptr) add_keys(outer->right); else return; 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); } |
#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; }
View | Download | Comments |
---|---|---|
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 |