Object-Oriented Programming


Templates in C++

Nicholas Wilt


Nicholas Wilt is a software engineer currently residing in eastern Massachusetts. His interests include photorealistic computer graphics, 80x86 assembler and C++ programming. Mr. Wilt welcomes constructive feedback about his work; he can be reached via CompuServe [75210, 2455] or in care of the editorial offices of CUJ.

Templates are a recent addition to the C++ programming language. By providing a way to define parameterized data types, they complement the mechanisms for reusability provided by inheritance. Templates come in two related flavors: function templates and class templates.

Function Templates

The function template in Listing 1 implements Insertion Sort as described by Bentley (1986). Listing 2 contains a program to test the Insertion Sort template.

A few points are worth mentioning about the InsertionSort function template. First, it does not declare a function called InsertionSort; rather, it describes a family of over-loaded functions named InsertionSort. If you call InsertionSort with an int *, the compiler generates an instance of the InsertionSort function template that sorts integers. If you call InsertionSort with another pointer type, the compiler generates another instance of the function that can sort that type. This approach to a generic implementation of Insertion Sort compares favorably to the qsort library function's approach, which requires a function call for every comparison. Templates let you implement a sorting routine that is both generic and efficient.

InsertionSort can be called for any user-defined class, as well as the primitive data types. If you call it with a user-defined class, you must be sure the copy constructor and assignment operator work properly, and you must overload operator< for the class.

Function templates can resolve many issues of code reusability. Insertion Sort is not a very involved algorithm (hence its use as an example), but template implementations of more complicated algorithms such as Quicksort pay off handsomely in reduced maintenance costs. Templates are useful in less generalized contexts as well. Template-based numerical algorithms allow the fast implementation that uses float and the stable implementation that uses double to share the same source code.

All the function template does is save you typing and maintenance. Unfortunately, every time you call a function template with a different argument for the parameter, the compiler generates another instance of the function template. As far as the resulting executable is concerned, you might as well have implemented separate InsertionSort functions. For applications where you apply a large variety of types to a function template, this can be an overwhelming use of space.

Class Templates

Class templates offer even greater potential for code reuse than function templates. They let you describe a family of related classes generically, then provide specific parameters later, as needed. This approach to generality is called a parameterized data type. The template defines a family of related classes, all with members and member functions of the same name that behave similarly. Here is a partial definition of a linked list class template to introduce you to the syntax:

template<class T>
   class LinkedList {
     void Insert(const T&);
     T *Query(const T&) const;
   };
Like function templates, the template definition itself does not define any classes. Classes result from instantiating the class template. The above template would be instantiated using the same angle brackets that were used to define it; that is, LinkedList<int> is a linked list of integers class, Linked-List<UserDefinedType> is a linked list of UserDefinedTypes class. Barring their origin, classes that result from instantiating a class template are just like other classes.

Templates are especially well-suited to implementing container classes. As an example, I have implemented a balanced binary tree class template. Balanced binary trees are tremendously useful data structures for keeping track of dynamic collections of ordered objects. Almost all operations on a balanced binary tree require only O(logN) time where N is the number of nodes in the tree. These operations include insertion, deletion, query, minimum, and maximum. In addition, if you have a pointer to a node in the binary tree, you can find the predecessor or successor of that node in O(logN) time. The balanced binary tree in this article is a red-black tree as described by Cormen et al. (1990).

The binary tree implementation makes use of three class templates: BinaryNode, Binarytree, and BinaryTreeIter. BinaryNode implements the node of a binary tree. Since a binary tree node is really the root of a subtree, BinaryNode implements most of the functionality required for binary trees. It contains some member functions that are protected, including LeftRotate, RightRotate, and DeleteFixup. These functions are used to balance the tree after an insertion or deletion, so they are usable only by classes that inherit from instances of BinaryNode.

BinaryTree encapsulates a single, balanced binary tree. It contains a pointer to a BinaryNode (the root of the tree), and makes available member functions such as Insert that are implemented in terms of BinaryNode.

BinaryTreeIter, an iterator class for BinaryTree, makes it unnecessary for users of BinaryTree to deal directly with BinaryNodes. Its constructors take a tree as input; the iterator then has a pointer to the root node of the binary tree. Subsequent operations operate on the subtree pointed to by this root node; the Min function, for example, finds the minimum node in the subtree and points the iterator at it. The Contents member function returns a pointer to the contents of the root node in the subtree, or 0 if the iterator is not pointing at anything.

BinaryTree, like the InsertionSort function template given earlier, has been designed to work with any class as long as operator< is defined.

Listing 3 contains btree.h, the definitions for the three class templates BinaryNode, BinaryTree and BinaryTreeIter. (The code to maintain a red-black tree is extensive, so the source code listing has not been provided in its entirety. Rather, Listing 3 contains the btree.h header, where the class templates are defined.)

Listing 4 contains the program that tests the three class templates that implement binary trees. It thoroughly exercises the BinaryTree<int> class by inserting, querying and deleting integers, and checking the invariants for the binary tree between operations. It also uses the BinaryTreeIter<int> class to traverse the nodes and ensure that they appear in the right order.

As with function templates, class templates save you typing and maintenance. But every time a class template is instantiated with different arguments, the compiler generates another whole instance of the class. Again, in situations where the compiler instantiates the template many times, this can be an overwhelming use of space.

Class Templates And Inheritance

Class templates work best when combined with inheritance. In the case of the BinaryTree template, you can extend its capabilities by having another class template inherit from it. This declares a family of classes who all inherit from related members of another family of classes. As an example, I'll develop a template implementation of the order statistics tree, an augmentation of the balanced binary tree as described by Cormen et al. (1990).

An order statistics tree supports all the operations that a binary tree supports, plus a very efficient Selection operation. The Selection operation chooses the ith element in the sorted order of an N-element array. For example, numbering from zero, the median of an array for odd N is the (N—1)/2th element. For a single selection operation, optimal O(N) algorithms have been derived. The order statistics tree supports selection in O(logN) time, where N is the number of elements in the tree. Since N elements must be inserted into the order statistics tree before performing selection, the order statistics tree takes O(NlogN) time for a single selection operation. This asymptotic runtime is not efficient compared to the linear algorithms; however, when the number of selection operations grows, the advantage offered by the O(logN) time for a single selection gets more noticeable.

The order statistics tree works well when the data sets from which it selects do not change very much between selections. A good application is the median neighborhood operation in image processing. The median operation performs noise reduction by replacing each pixel with the median of its neighbors (including itself, so a total of nine pixels are considered). You might implement this algorithm by reading the pixel values for each pixel of its neighbors and itself, computing the median, and replacing the pixel value. But the data set for the next pixel is similar to the data set for the pixel just computed. (They share three elements.) If you maintain an order statistics tree to compute the median values, pseudocode for a single pixel would look like:

Compute median using O(lgN) selection of the OS tree
Delete leftmost 3 pixel values from OS tree
Insert rightmost 3 pixel values from OS tree
The 3x3 median problem has been studied extensively, and the above technique is probably not the most efficient. See Paeth (1990) for an implementation that requires at most 19 comparisons per pixel. For larger neighborhoods, however, more of each pixel's neighborhood is shared with those of its neighbors. At some point the order statistics tree becomes a good way to solve large-kernel median problems.

Implementing the Order Statistics Tree

To implement the order statistics tree, I derive OSNode and OSTree class templates from the BinaryNode and BinaryTree templates. The syntax is:

   template<class T>
   class OSNode : public BinaryNode<T> {
   // class definition
   };
This declares a template for a family of classes called OSNode. All of the classes in the family inherit publicly from BinaryNode<T>. OSNode is a binary tree node that incorporates the necessary modifications to make it a node in an order statistics tree. Whenever possible, it calls the BinaryNode implementations of its member functions to maintain the order statistics tree.

This implementation is inconvenient because the left, right, and parent pointers are declared as BinaryNode<T> *s in the BinaryNode class template. C++ will not automatically promote a pointer to an inherited class to a pointer to its base class. So, because the OSNode class template considers these pointers to point to OSNode<T>s, the code for OSNode member functions contains many typecasts from BinaryNode<T> * to OSNode<T> *.

OSTree inherits from BinaryTree just like OSNode inherits from BinaryNode. OSTree does not need to overload as many functions, however, since the node templates do most of the work. For example, the BinaryTree version of DeleteItem works just fine for OSTree.

Listing 5 gives os.h, the header for the order statistics tree implementation. Listing 6 gives os.cpp, the C++ program that tests OSTree.

Conclusion

Function and class templates offer a clean way to write reusable, highly maintainable code, while retaining the efficiency of reimplementing the functions or classes as the need arises. Templates are by no means a panacea, especially when a large number of instances are needed, but they can be a viable option among the innumerable options presented by C++.

(C) 1993 Nicholas Wilt.

Bibliography

Arvo, J. and D. Kirk. 1989. In An Introduction to Ray Tracing. Boston: Academic Press.

Bentley, J. 1986. Programming Pearls. Reading, MA: Addison-Wesley.

Cormen, T., C. Leiserson and R. Rivest. 1990. Introduction to Algorithms. Cambridge, MA: MIT Press.

Paeth, A. 1990. "Median finding on a 3x3 grid,". Graphics Gems. Boston: Academic Press.

Preparata, F. P. and M. I. Shamon. 1985. Computational Geometry. New York: Springer-Verlag.