16 Templates PDF
16 Templates PDF
16 Templates PDF
Some of the most reusable pieces of code are general container classes, such as arrays,
lists, stacks, and binary trees. The major impediment to reusing container classes is
having problems dealing with different types. To a compiler, a stack of integers is
different from a stack of strings or a stack of airports. What is needed for container
classes is polymorphism which is a blindness to the exact type youre dealing with. In
theory, a stack behaves the same regardless of what type it holds it supports push and
pop operations regardless of what type it is holding. There are two main hitches to
providing a stack of any type in C or C++ (up until today, that is). First, an integer is
not necessarily the same size as an airport, so how big do you make each element of a
stack if the same stack can hold an airport or an integer? Second, the compiler wants to
know what type youre passing to and returning from the stack functions.
A Simple StringQueue
Let's revisit what I assume is a familiar abstraction from CS106B or CS106X: the Queue.
Rather than make the abstraction fully polymorphic in the traditional C manner, lets
work with a specialized version of a queue that stores only strings. While examining
the code, understand that the queue abstraction, like many other abstractions, is that of a
container classit in no way depends on the functionality of the string class. In fact,
the only detail the Queue class needs at compile-time is exactly how big a string is;
that's it!
class Queue
{
public:
Queue();
~Queue();
Queue::~Queue()
{
Node *next;
while (head != NULL) {
next = head->next;
delete head; // doesn't delete the next field recursively
head = next;
}
}
The more interesting code, however, presents itself within the enqueue, dequeue, and
peek methods.
tail = newTail;
}
string Queue::dequeue()
{
assert (head != NULL);
delete old;
return str;
}
private:
struct Node {
Element elem;
struct Node *next;
Node(const Element& elem, struct Node *next) :
elem(elem), next(next) {}
} *head, *tail;
};
The definition of the class uses a template parameter, which in this case is the element
type. A template parameter is often, but doesnt have to be, the element type of a
container. In this example, class is the parameter type, and Element is the parameter
name. Template parameters can be other types, such as integers. You can also have
more than one template parameter. Within the class definition, we can use Element
to represent our generic contained type. Really, this is a placeholder which gets filled
in (at compile-time) when we instantiate a Queue object. We have to use the template
specifier when we implement the Queue member functions. The constructor would look
like this:
The more involved enqueue and dequeue operation would look like this:
tail = newTail;
4
delete old;
return elem;
}
Instantiating Templates
To instantiate a template, you simply declare an Queue like you normally would, save
the fact that you must specify exactly what data type should be stored in the Queue.
Once the compiler sees this data type, it generates code on your behalf and compiles it as
if you hand hand-typed the specialized Queue type in yourself.
int main()
{
Queue<int> intQueue;
Queue<char> charQueue;
while (!intQueue.empty()) {
cout << setw(2) << intQueue.dequeue() << " " << charQueue.dequeue() << endl;
}
}
Template Functions
Global functions can be used with templates as well. This is useful for utility functions
that can apply to any type, as well as for implementing user-defined operators for
classes. Suppose we wanted to write a max function that would return the larger of its
two arguments. We could write this like this:
Note that every type we use with this function must provide an operator> for this
function to work. If there is no operator> for the type provided, there will be a
compile-time error. We can simply call the max function like this:
We dont have to specify the template argument in these cases because the compiler
figures it out for us. However, we could provide the template argument it if we wanted
to, and we have to provide it if the compiler cant figure it out otherwise.
Specialization
The max code brings up an interesting point. Consider comparing two C-strings with
the max function. If we wrote:
When the generic source code of a template isn't exactly what we want, we can override
the template behavior. In doing so, we need to specify exactly what type the
specialization is designed for (using the same template syntax while specifying an actual
type instead of a placeholder) and then provide the specialized implementation.
Effectively, we are providing the routine as if the compiler had generated it from the
template. When the compiler goes to generate the code for a function, it uses the code
we provided instead.
In order to provide a specialized version of the max function for strings, we need to
provide the routine like this:
Now if we ever call max with strings, the specialized version will be called.
Because the compiler needs to generate separate copies of the template function or
template class code, you dont simply add the .cc file to your project or Makefile.
Instead, you #include the proper header file which should contain both the class or
function definition and the implementation. I generally structure my templates like this
I do this because I dont like to put the implementation of any classes or functions in a
header file. The template still represents an abstraction, so in the spirit of abstraction and
information hiding, I place all the implementation code in a .cc file as if it were a normal
class .