13.3. Template Classes

Not every function benefits from being made into a template. Likewise, it's not useful to make every class into a template. But there is one kind of class for which the C++ designers created templates: containers (aka collections in Java). Instances of container or collection classes hold and organize many data items. Think of a container as a basket into which you can place data. The type of data placed in the basket can be simple, like integers or doubles, or as complex as the most complex class you can create. The container doesn't "care."

Containers and collections represent various kinds of data structures. "A data structure is a specialized format for organizing and storing data... Any data structure is designed to organize data to suit a specific purpose so that it can be accessed and worked with in appropriate ways. In computer programming, a data structure may be selected or designed to store data for the purpose of working on it with various algorithms." Some data structures are simple enough to be implemented directly by computer programming languages. For example, arrays, data files, structures, and classes. Other data structures are too complicated to be language primitives. These more complex data structures include stacks, lists, trees, hash tables, etc. But these complex data structures are also too useful to ignore and are provided by classes organized into language libraries.

Although the list of possible data structures is quite long, our task here is to explore the C++ syntax needed to create and to use data structures based on "templatized" classes. We begin by comparing C++ containers with Java collections.

Containers and Collections

Although C++ containers and Java collections go by different names, they provide the same services and are used to solve the same problems. Furthermore, their underlying implementations are named differently, but their syntax is nearly identical. C++ bases its containers on templates. Templates allow programmers to specify the data type of the stored data when they create the container. Similarly, Java bases its collections on generic classes, allowing programmers to specify the stored data type when creating the collection.

The main task, when using library data structures, is specifying the kind of data that the data structure will store. We'll use a data structure called a vector (C++ class name) or Vector (Java class name) to demonstrate how programmers do this. Vectors are a kind of "intelligent" array that dynamically manages their size: it can automatically grow as necessary, and programmers can shrink them when needed. But you should note that allowing a vector to grow automatically can be an expensive (in time) operation when the vectors become large. For this reason, the following examples begin with large vectors.

Each example creates a vector and inserts pseudo-random numbers into it to demonstrate how to use data structures based on templates and generic classes. Generic classes in Java can only store objects - they cannot store fundamental data types (like ints and doubles). Consequently, the random numbers in the Java program are first stored or wrapped in an instance of the Integer wrapper class. The program transparently creates the Integer wrapper objects and stores the random numbers in them; this process is called "autoboxing." Alternatively, C++ templates can store simple data types as easily as they can store instances of structures or classes.

import	java.util.Vector;
import java.util.Random;

public class VectorEG
{
	public static void main(String[] args)
	{
		int	size = 1000000;

		Random	rand = new Random();

		Vector<Integer>	data = new Vector<>(size);
		//ArrayList<Integer>	data = new ArrayList<>(size);

		long start = System.currentTimeMillis();

		for (int i = 0; i < size; i++)
			data.add(rand.nextInt());

		long end = System.currentTimeMillis();

		System.out.println(end - start + " milliseconds");
	}
}
Creating and using a Java Vector. The Vector class has evolved extensively throughout Java's lifetime: The Vector class (included in the initial Java API) is said to be thread-safe, which means that two or more threads sharing the Vector will access the data one at a time (i.e., they will play nicely together). For us, this means that the overhead needed to make the Vector thread-safe causes it to run more slowly than if it was not thread-safe. When thread safety is not needed, it is common to use the newer ArrayList class. Both classes have the same public interface and basic behaviors, but the ArrayList operates faster because it does not incur the overhead of thread safety.
#include <iostream>
#include <stdlib.h>
#include <time.h>
#include <sys/timeb.h>
#include <vector>					// the templatized source code
using namespace std;


// convert a time structure to milliseconds

inline	double	floattime(_timeb& t)
{
	return t.time + t.millitm / 1000.0;
}


int main()
{
	const	int	SIZE = 1000000;
	_timeb	start_time;
	_timeb	end_time;

	vector<int>	v(SIZE);			// automatic vector for ints
	//vector<int>*	v = new vector<int>(SIZE);	// dynamic vector for ints

	srand((unsigned)time(nullptr));
	_ftime_s(&start_time);				// get time at start

	for (int i = 0; i < SIZE; i++)
		v[i] = rand();				// automatic version
		//(*v)[i] = rand();			// dynamic, pointer version

	_ftime_s(&end_time);				// get time at end

	cout << floattime(end_time) - floattime(start_time)
		<< " milliseconds" << endl;
		
	// delete v;					// if created with new

	return 0;
}
Creating and using a C++ vector. Creating an object (i.e., defining a variable) from a template-based class is similar to creating an object from any class but with one additional step. When creating a container, programmers must also specify the kind of data stored in the container and the name of that type replaces the template variable. The type name appears in angle brackets following the class name: vector<int>. Or, we could just as easily make a vector capable of storing instances of the person class: vector<person>. C++ vectors can be created automatically on the stack or dynamically on the heap with the new operator. Either way, their usage, and performance are nearly identical.

The Java code appearing in Figure 1 is more compact and easier to follow than is the C++ version presented in Figure 2. Java has one more distinct advantage beyond compactness: portability. The code in Figure 1 will run without modification on any computer where a Java Runtime Environment (JRE) is installed. The code in Figure 2 is not as portable because it contains a system call.

An operating system provides a host environment for every program running on a computer. The operating system manages the hardware resources and provides services to the running programs. Programs can request services through system calls, which are function calls to functions defined in the host operating system. Most of the programs that we have studied so far request operating system services through language-specific library functions (e.g., operator<< and operator>>). But C++ doesn't provide a function for getting the system time. So, we must request that information directly from the operating system with a system call:  _ftime_s. System calls are not standardized between operating systems, so while  _ftime_s works on a Windows system, it's not appropriate for all systems. In this case, it's easy to modify the program of Figure 2 so that it runs on Unix, Linux, and macOS with only two small substitutions:

All times in milliseconds
C++Java
DefaultOptimizedArrayListVector
0.026 0.019 28 44
0.028 0.019 28 44
0.027 0.020 27 44
0.027 0.019 28 44
C++ versus Java performance. Although it is often easier to write a Java program than to write the same program in C++, C++ programs typically out perform Java. The table lists the run times of the programs listed in Figures 1 and 2. The times are measured on the same computer with the same loads. Many C++ compilers implement various code-generation options; the C++ column presents the run-times for the default settings and with the speed optimizations turned to their highest settings. Each program is run four times to help eliminate any anomalous timing due to uncontrollable background processes.

Making Template Classes

A stack is a classic data structure and a prime candidate for implementation as a template-based container. We first studied stacks in detail in the chapter about arrays. Since then, we have used them in several examples. Most recently we created a stack class (see Figures 2 and 5) and used it to demonstrate the this pointer. Basing the stack on templates is a simple modification. The following figure illustrates the basic and most common features of a template class and lists the steps needed to convert a class to a template.

Caution

For many years, only the keyword "class" was allowed when creating template functions or template classes. Then the ANSI standard was updated to allow either "class" or "typename" when creating template functions but not when creating template classes. Newer C++ compilers (e.g., Visual Studio 2017+ and Linux) now allow both keywords to create template functions and template classes. The "class" keyword always works for both and is used in all the examples.

#include <iostream>
using namespace std;


template <class T>
class stack
{
	private:
		static const int SIZE = 100;

		T	st[SIZE];
		int	sp = 0;

	public:
		void	push(T data);
		T	pop();
		int	size();
		T	peek();
};

template <class T>
void stack<T>::push(T data)
{
	if (sp < SIZE)
		st[sp++] = data;
	else
		cerr << "ERROR: stack is full" << endl;
}

template <class T>
T stack<T>::pop()
{
	if (sp > 0)
		return st[--sp];
	else
	{
		cerr << "ERROR: stack is empty" << endl;
		return '\0';	// no good value to return; use exceptions
	}
}

template <class T>
int stack<T>::size()
{
	return sp;
}

template <class T>
T stack<T>::peek()
{
	return st[sp - 1];
}
stack.h: a template version of the stack class.
  1. The class specification and the functions are all placed in a header file
  2. The template variable T is introduced either by template <class T> or alternately by template <typename T>, which is placed before the class and before each function.
  3. After the code is expanded, the type name becomes part of the class name, which is accomplished by adding the template variable <T> to the unexpanded class name
  4. Replace all occurrences of the stored data type with the template variable T
#include  <iostream>
#include  "stack.h"
using  namespace  std;

int main()
{
	stack<int>  s;

	s.push(10);
	s.push(20);
	s.push(30);

	cout << s.pop() << endl;
	cout << s.pop() << endl;
	cout << s.pop() << endl;

	return 0;
}
#include  <iostream>
#include  "stack.h"
using  namespace  std;

int main()
{
	stack<person>	p;
	person		x("Alice");
	person		y("Dilbert");
	person		z("Wally");

	p.push(x);
	p.push(y);
	p.push(z);

	p.pop().display();
	p.pop().display();
	p.pop().display();

	return 0;
}
(a)(b)
Using a template class. When instantiating a template class, programmers specify the data type that replaces the template variable <T> inside a pair of angle brackets. Any legal data type, including a class name, may be specified here.
  1. Creating a stack to store integers.
  2. Creating a stack to store person objects.

Constant Value Expressions

It is also possible to pass constant values into templates. The previous example fixed the stack size at 100 elements. This size wastes many elements when only a small stack is needed, but it fails altogether when a stack with more than 100 elements is required. A better solution is to allow the programmer using the stack to specify its size when creating it. This approach requires modifying the template code and the application code creating the container.

Template Code
template <class T, int SIZE>
Application Code
stack<int, 10> s;
Template Code
template <class T, int SIZE = 100>
Application Code
stack<int, 10> s;
stack<int> s;
(a)(b)
Creating and using template constants.
  1. A template class that requires programmers to specify a data type and a stack size.
  2. A template class with a default size. Programmers must specify a data type, but they may override or accept the default size.
#include <iostream>
using namespace std;

template <class T, int SIZE = 100>
class stack
{
	private:
		T	st[SIZE];
		int	sp = 0;

	public:
		void	push(T data);
		T	pop();
		int	size();
		T	peek();
};

template <class T, int SIZE>
void stack<T, SIZE>::push(T data)
{
	if (sp < SIZE)
		st[sp++] = data;
	else
		cerr << "ERROR: stack is full" << endl;
}

template <class T, int SIZE>
T stack<T, SIZE>::pop()
{
	if (sp > 0)
		return st[--sp];
	else
	{
		cerr << "ERROR: stack is empty" << endl;
		return '\0';	// no good value to return; use exceptions
	}
}

template <class T, int SIZE>
int stack<T, SIZE>::size()
{
	return sp;
}

template <class T, int SIZE>
T stack<T, SIZE>::peek()
{
	return st[sp - 1];
}
Stack size specified by a template argument. In this version, a template variable replaces static const int SIZE = 100; appearing in Figure 4. This version also specifies a default value of 100, which the user can accept or replace, as is illustrated in the next figure. Notice that if a default value is specified, = 100, it only appears in the template statement placed above the class and not in the similar statements placed above each function.
#include  <iostream>
#include  "stack.h"
using  namespace  std;

int main()
{
	stack<int, 10>	s;
	stack<double>	d;

	s.push(10);
	s.push(20);
	d.push(2.7);
	d.push(3.1459);

	cout << s.pop() << endl;
	cout << s.pop() << endl;
	cout << d.pop() << endl;
	cout << d.pop() << endl;

	return 0;
}
Creating an instance of a class with constant value expressions. The user may specify the stack size (stack s) or accept the default stack size of 100 (stack d).