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.
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"); } }
Vector
class has evolved extensively throughout Java's lifetime:
Vector
were upcast to instances of the Object
class when added to the Vector
, which required programmers to appropriately downcast them when they were retrieved.Vector<Integer> data = new Vector<Integer>();
Vector<Integer> data = new Vector<>();
<>
are called the diamond operator.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; }
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:
_ftime_s
→ ftime
_timeb
→ timeb
C++ | Java | ||
---|---|---|---|
Default | Optimized | ArrayList | Vector |
0.026 | 0.019 | 28 | 44 |
0.028 | 0.019 | 28 | 44 |
0.027 | 0.020 | 27 | 44 |
0.027 | 0.019 | 28 | 44 |
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.
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]; }
T
is introduced either by
template <class T>
or alternately by template <typename T>
, which is placed before the class and before each function.<T>
to the unexpanded class nameT
#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) |
<T>
inside a pair of angle brackets. Any legal data type, including a class name, may be specified here.
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.
|
|
(a) | (b) |
#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]; }
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; }