You are on page 1of 20

INTRODUCTION

This paper introduces templates, the C++ facility for implementing generic classes and
functions. It also introduces how to use templates to implement container and iterator
classes. A container is a data structure, such as a list, queue or vector, that holds other
objects. An iterator is a mechanism for visiting each object in a container. This paper
presents alternative implementations for containers and iterators, with particular emphasis
on the approach used in the C++ Standard Template Library (STL).

FUNCTION TEMPLATES

A generic function is a single function that takes the place of many overloaded functions
with nearly identical definitions. In C++, you implement a generic function as a function
template. A function template declaration acts as a virtually unbounded set of overloaded
function declarations.

For example, a single function template declaration such as:

template <class T>


void swap(T &a, T &b);

can replace the declarations for many overloaded functions such as:

void swap(int &a, int &b);


void swap(string &a, string &b);

The corresponding function template definition is:

template <class T>


void swap(T &a, T &b)
{
T temp = a;
a = b;
b = temp;
}

In both the template declaration and definition, <class T> is a template parameter list,
which specifies T as a template type parameter.
The keyword class in the template parameter list suggests that T can be a class type
only. Actually, T can be a non-class type (such as int) as well as a class type (such as
string). Standard C++ added the keyword typename as an alternative to the key-
word class in template parameter lists, as in:

template <typename T>


void swap(T &a, T &b);

Within a template definition, a template type parameter behaves like any other type
name. As with any type name, a template type parameter has a scope:

template <typename T> // T's scope begins here


void swap(T &a, T &b)
{
T temp = a;
a = b;
b = temp;
} // T's scope ends here

INSTANTIATION AND SPECIALIZATION

It’s difficult to explain how templates work without using the terms instantiation and spe-
cialization. Unfortunately, the C++ Standard isn’t very clear about what these terms
mean. Consequently, even “experts” use these terms inconsistently.

Compilers generate function declarations and definitions from a function template as


needed to satisfy function calls. Instantiation is the process of generating a declaration or
definition from a template. A declaration or definition generated from a template is
called a specialization.

A function call that names a function template can specify the template argument explic-
itly. For example,

string s, t;
...
swap<string>(s, t);

Here, swap<string> is a specialization of the function template swap with template


argument string. The compiler then instantiates a definition for:

void swap<string>(string &a, string &b);


to satisfy the call to swap<string>.

Compilers can often deduce the template argument from the argument(s) to a call that
names a function template. In that case, the call need not specify the template argument
explicitly. For example:

int i, j;
string s, t;
...
swap(i, j); // calls swap<int>
swap(s, t); // calls swap<string>

CLASS TEMPLATES

A generic class is a single class that can be adapted to specify similar, yet distinct, types.
A generic class can take the place of many classes with similar names and nearly ident i-
cal definitions. In C++, you implement a generic class as a class template. A class tem-
plate definition acts as a virtually unbounded set of type definitions.

For example, class rational represents a rational number as a pair of ints.

class rational
{
public:
rational(int n = 0, int d = 0);
...
private:
int num, denom;
};

Some applications that perform rational arithmetic may need more than int precision;
others may need less. Implementing rational as a template named rational<T>
provides a more flexible design. A programmer using the template can select any integer
type as the type of the numerator and denominator by substituting that type for the tem-
plate type parameter T. For example, rational<int> is the type name for rational
numbers with int precision, and rational<unsigned long int> is the type
name for rational number with unsigned long int precision.

The template class definition looks something like:


template <typename T>
class rational
{
public:
rational<T>(T n = 0, T d = 1);
...
private:
T num, denom;
};

The class template definition defines a class template named rational<T>. The func-
tion declared in the class has the same name as the class, and so that function is a con-
structor.

Given the definition just above, a declaration such as:

rational<int> ri(10);

declares ri as an object of type rational<int> with value 10/1.

A type name such as rational<int> names a class template specialization. More


precisely, rational<int> is the name of the type that is the specialization of template
rational for the particular template type argument int. Syntactically, a class tem-
plate name followed by a template argument list, as in:

rational<long int>

is called a template-id:

You can use a template-id anywhere you can use any other class name. For instance,

typedef rational<unsigned int> unsigned_rational;

defines unsigned_rational as an alias for rational<unsigned>. As another


example,

rational<short int> *p;

declares p as a pointer to a rational specialized for short int.


MEMBERS OF CLASS TEMPLATES

As with non-template classes, the definition for a member function of a class template
can appear inside the class definition. Here, the constructor is defined inside the class
definition:

template <typename T>


class rational
{
public:
rational<T>(T n = 0, T d = 1)
: num(n), denom(d)
{
}
...
};

The member function definition can also appear outside the class definition, as in:

template <typename T>


rational<T>::rational<T>(T n, T d)
: num(n), denom(d)
{
}

When it appears outside the class template definition, a member function definition must
begin with a template heading such as:

template <typename T>

which must have the same number of type parameters as the heading that begins the class
definition. In addition, the member function name must be a fully-qualified name of the
form:

template-id::function-name

For example, in:

template <typename T>


rational<T>::rational<T>(T n, T d)

the full-qualified function name is rational<T>::rational<T>.


In the scope of class template rational<T>, rational by itself is equivalent to ra-
tional<T>. For example, inside the class template definition for rational<T>,
these are equivalent:

rational<T>(T n = 0, T d = 1);
rational (T n = 0, T d = 1);

A appearing name to the right of rational<T>:: is also in the scope of ra-


tional<T>. Therefore, when the definition for the constructor appears outside its class
definition, the full-qualified function name rational<T>::rational<T> can be
written as rational<T>::rational. The second <T> is optional.

template <typename T>


rational<T>::rational(T n, T d) // 2nd <T> is omitted
: num(n), denom(d)
{
}

A class template can have static data members. If that static data member is used any-
where in the program, its definition must appear outside its class definition. For example:

template <typename T>


class widget
{
...
static unsigned counter;
};

declares counter as a static data member of widget<T>. The definition for that
counter looks like:

template <typename T>


unsigned widget<T>::counter = 0;

A class template can have members that are types. For example, many STL container
classes have a member type called iterator, declared as:

template <typename T>


class vector
{
public:
class iterator; // member type declaration
...
};
Outside the class template definition, you refer to the iterator type by its fully-
qualified name. For example,

vector<double>::iterator i;

declares i as an object of type iterator for a vector<double>.

FRIENDS OF CLASS TEMPLATES

A class template can declare friends, as in:

template <typename T>


class rational
{
friend ostream &operator<<
(ostream &, rational const &);
...
}

Again, whenever it appears in the scope of rational<T>, the name rational is


equivalent to rational<T>. Thus, the friend declaration for operator<< is actually
a declaration for a function template with type parameter T:

friend ostream &operator<<


(ostream &, rational<T> const &);

When it appears outside the class definition, the definition of operator<< has the
form:

template <typename T>


ostream &operator<<(ostream &s, rational<T> const &r)
{
...
}

A function that’s a friend of a template is itself a template only if it has a function pa-
rameter that depends on a template parameter. For example, the second parameter of
operator<< depends on the class template parameter T, so operator<< is a tem-
plate.

On the other hand, in:


template <typename T>
class widget
{
friend void f(int);
...
};

f is a non-template function that is a friend of every specialization of class widget<T>.


It’s not clear that this is good for anything, but C++ does allow it.

TEMPLATE PARAMETERS AND ARGUMENTS

A template argument can be any type name, not just an identifier. For example,

list<char *> names;

declares names as an object of type list specialized with elements of type char *.
A template argument can even be a template-id. Here,

list< rational<long> > ratios;

declares ratios as a list specialized with elements of type rational<long>. If


you omit space after rational<long>, you get a compile error:

list<rational<long>> ratios; // no! >> is a shift operator

A template parameter can be a non-type parameter rather than a type parameter, as in:

template <size_t N>


class bits;

bits is a class that represents a set of N bits. <size_t N> is the template parameter
list. N is a template non-type parameter.

You can specialize class bits for any value of type size_t (an unsigned integer type).
For example,

bits<32> reg;

declares reg as an object representing a 32-bit set. A non-type parameter must have one
of the following types:
• integral
• enumeration
• pointer to object, function or member
• reference to object or function

A template can have more than one parameter. For example,

template <typename T, typename U>


class pair;

declares a class representing a pair of values of types T and U. Using the pair template:

pair<string, double> lookup(string const &);

declares lookup as a (non-template) function that accepts a string and returns a


string paired with a double.

A template can also have default arguments, as in:

template
<typename T, typename X = size_t>
class vector
{
public:
vector(X, X);
...
};

This example defines a template for a vector with elements of type T and indices of
type X. X has size_t as its default, so that:

vector<double> v(1, 10);

defines v as a vector<double, size_t> with indices in the range 1 to 10, inclu-


sive.

vector<double, int> w(-100, 100);

defines w as a vector<double, int> with indices in the range -100 to +100, in-
clusive.

FUNCTION PARAMETERS

For the most part, a function declared as:


void f(T const &t);

has the same outward semantics as a function declared as:

void f(T t);

That is, with either declaration, calls to f look the same. For example,

T x;
...
f(x); // might pass x by value or by reference-to-const

If you were to implement f as a template, how should it pass its parameter? By value, or
by reference-to-const? In answering the question, consider how the template function
will perform when you specialize it for various types, both large and small.

In general, passing a large object by reference-to-const can be much cheaper than


passing it by value, so passing by reference-to-const should be the safer bet. Passing a
small object by reference may be a little more expensive than passing that object by
value, but it’s never much more expensive. In fact, if the function call is inline, the com-
piler can even optimize away the reference binding so that passing can reference can be
as cheap as passing by value.

Therefore, you should favor passing an argument of parameterized type T to a function


template as type T const & rather than as plain T, as in:

template <typename T>


void f(T const &x);

For types where passing by value is more efficient, you can define overloaded functions
with the same name as the template:

void f(char c)
{
...
}

A function call uses a template only if it can’t find an overloaded function that’s a better
match:
char c;
string s;
...
f(c); // calls f(char)
f(s); // calls f<string>(...)

ARGUMENT CONSTRAINTS

The following template appears on the surface to work for any type T:

template <typename T>


T abs(T const &x)
{
return x > 0 ? x : -x;
}

In reality, it works only for certain types. In particular, it works only for types T such
that:
• you can compare a T to 0 using >
• you can apply unary - to a T
• you can construct one T by copying another T (for the return value)

Such requirements are implied argument constraints. Some languages have a notation
for explicitly specifying the constraints on the template argument(s); C++ does not. This
is both good and bad. Sometimes you can apply a template to types that the author never
imagined. On the other hand, the diagnostics you get when you violate a constraint can
be pretty obscure.

COMPILATION MODELS

A C++ translator (compiler and linker) automatically instantiates definitions as needed


for each (function and class) template specialization. Not surprisingly, different transla-
tors use different approaches to support instantiation. The differences affect whether you
place definitions in headers or in source files. Unfortunately, these differences make it
hard to write libraries of templates for multiple platforms.

The two most common template compilation models are known as the separation model
and the inclusion model:
• Under the separation model, you place the template class definition and inline me m-
ber function definitions in a header (as you do with non-template classes). You place
all other template class member function definitions and static data member defini-
tions in one or more source files.
• Under the inclusion model, you place the template class definition and all of its me m-
ber function definitions (inline and non-inline) and static data member definitions in
the header.

The C++ standard supports both of these models. However, the standard requires that
separately compiled template definitions must be declared with the keyword export.
export was added to the standard late in the process. It may be years before most com-
pilers actually do what the standard says.

CONTAINERS

A container (or collection) is a data structure, such as a list, stack, tree, or vector, that
holds objects of other types. For any types T and U, the code for a given container hold-
ing T objects is nearly identical to the code for the same kind of container holding U ob-
jects. Again, templates can eliminate most of the duplication.

As an example, consider a very simple class for a first-in-first-out (FIFO) queue with
elements of arbitrary type T. The basic queue operations are:
• add an element to the back of the queue
• remove an element from the front of the queue
• test if the queue is empty
There are various ways to implement the basic operations. The following example mim-
ics the design used in the STL.

queue<T> provides the following operations as members:


• queue<T>() (the default constructor) creates an empty queue.
• ~queue<T>() (the destructor) discards the queue’s contents.
• bool empty() const returns true if there are no element in the queue.
• T &front() returns a reference to the queue’s front.
• void pop_front() discards the queue’s front element.
• void push_back(T const &e) appends element e to the back of a queue.

Our first version of queue<T> also provides a non-member operator:


• ostream &operator<<(ostream &os, queue<T> const &q) writes
the elements in queue q to ostream os.

queue<T> uses a singly-linked list implementation. A queue holds pointers to the first
and last cell in the list. A cell holds an element of type T and a pointer to the next cell.

Here’s a first version of the class definition:


template <typename T>
class queue
{
friend ostream &operator<<
(ostream &, queue<T> const &);
public:
queue<T>();
~queue<T>();
bool empty() const;
T &front();
void pop_front();
void push_back(T const &);
private:
struct cell
{
cell(T const &v, cell *p)
: value(v), next(p)
{
}
T value;
cell *next;
};
cell *first, *last;
};

And here are the corresponding member function definitions:

template <typename T>


queue<T>::queue<T>()
: first(NULL), last(NULL)
{
}

template <typename T>


queue<T>::~queue<T>()
{
while (first != NULL)
{
cell *p = first;
first = first->next;
delete p;
}
}
template <typename T>
bool queue<T>::empty() const
{
return first == NULL;
}

template <typename T>


T &queue<T>::front()
{
return first->value;
}

template <typename T>


void queue<T>::pop_front()
{
cell *p = first;
first = first->next;
if (first == NULL)
last = NULL;
delete p;
}

template <typename T>


void queue<T>::push_back(T const &v)
{
cell *p = new cell(v, NULL);
if (first == NULL)
first = p;
else
last->next = p;
last = p;
}

template <typename T>


ostream &operator<<(ostream &os, queue<T> const &q)
{
queue<T>::cell *p = q.first;
for (; p != NULL; p = p->next)
os << ' ' << p->value;
return os;
}
Here is a code fragment that illustrates how to remove the front element from a
queue<string>:

string s;
queue<string> q;
...
if (!q.empty())
{
s = q.front();
q.pop_front();
}

ITERATION FUNCTIONS

The expression cout << q displays the values in queue q. Each value appears in its
default format preceded by a single space. Unfortunately, queue<T>’s operator<<
is not very flexible in that it does not allow any control over formatting.

operator<< is actually a very specific form of a general function that performs some
operation on each element in the container. You can implement this more general capa-
bility as a member function called for_each. for_each is an iteration function that
applies a specified function to each element in the container. For instance:

q.for_each(put);

applies a put function to every element in queue q.

The declaration for for_each replaces the declaration for operator<< in the class
definition:

template <typename T>


class queue
{
public:
...
void for_each(void (*)(T &));
...
};

The corresponding definition for for_each is:


template <typename T>
void queue<T>::for_each(void (*f)(T &))
{
queue<T>::cell *p = first;
for (; p != NULL; p = p->next)
(*f)(p->value);
}

Here’s how you might use for_each to write the strings in a queue formatted so that
each string is enclosed in single quotes and preceded by a space. First, define:

ostream *qos;

void put(string &s)


{
*qos << " '" << s << "'";
}

This put function writes the single string v enclosed in single quotes and preceded by a
space. You can use put to define operator<< as a non-member:

ostream &operator<<(ostream &os, queue<string> &q)


{
qos = &os;
q.for_each(put);
return os;
}

In the presence of these definitions,

queue<string> q;
...
cout << q;

displays each string in q enclosed in single quotes and preceded by a space.

ITERATOR CLASSES

Iteration functions can be effective, but tend to be inflexible and inconvenient. Extract-
ing statements from a loop body placing them into a separate function is a somewhat
“unnatural” way to write a loop. Also, passing additional parameters through the itera-
tion function to the applied function is difficult.
Iterator classes offer a more flexible way to apply an operation to each element in a con-
tainer. An iterator class is an abstract type for objects that can locate (“point” to) each
element in a container. Together, the container and its iterator provide functions (not
necessarily distinct) to:
• obtain the “address” of the first element in the container
• “increment” an iterator to “point” to the next element in the container
• determine when there’s no next element to “point” to

An iterator class is actually part of the public interface of its container class. Thus, it’s
customary to declare an iterator class as a public member of its container class. When the
queue’s iterator is a public member of the queue class template, the fully-qualified name
for a queue iterator is queue<T>::iterator.

There are many ways to design the iterator interface. The C++ standard library includes
a number of container class templates known collectively as the STL (Standard Template
Library). The following queue iterator is modeled after an STL iterator.

A loop that does something with each element in q of type queue<string> looks
something like:

queue<string>::iterator p;
p = q.begin();
while (p != q.end())
{
// do something with *p
++p;
}

You should think of an iterator as a pointer, such that:


• *p yields the object it “points” to
• ++p advances p to “point” to the next element

In addition to the iterator type, class queue<T> provides public members:


• begin() const returns an iterator positioned to start traversing a queue.
• end() const returns an iterator positioned beyond the end of a queue.

queue<T>::iterator provides a generated copy constructor, along with:


• operator!= compares two iterators for inequality.
• operator++ (prefix only) advances an iterator to the next element in a queue.
• operator* returns the element referenced by an iterator.

Here’s the template class definition:


template <typename T>
class queue
{
private:
struct cell; // the definition appears later
public:
queue();
~queue();
bool empty() const;
T &front();
void pop_front();
void push_back(T const &);
class iterator;
friend class iterator;
iterator begin() const;
iterator end() const;
class iterator
{
// details appear later
};
private:
struct cell
{
// same as always
};
cell *first, *last;
};

Members of a class normally have access to the non-public members of that class. A
nested class is a member of its enclosing class, but a nested class does not have access to
the non-public members of its enclosing class. Therefore, the queue<T> class defini-
tion must include the following declarations:

class iterator;

which introduces iterator as a nested class, and:

friend class iterator;

which declares queue<T>::iterator as a friend of queue<T> so that iterator


can access type queue<T>::cell.

The queue<T>::iterator class definition looks like:


class iterator
{
friend class queue<T>;
public:
int operator!=
(const iterator &i) const
{
return pc != i.pc;
}
T &operator*()
{
return pc->value;
}
iterator &operator++()
{
pc = pc->next;
return *this;
}
private:
iterator(cell *p)
: pc(p)
{
}
cell *pc;
};

The underlying implementation of an iterator is just a single cell *, which can


point to each cell in a queue<T>. The iterator class provides a private construc-
tor iterator(cell *p) which turns a cell * into an iterator. The construc-
tor exists only to implement the queue<T>’s begin and end member functions. Users
of queue<T> should not use this constructor (because they should not have access to
queue<T>::cell). Therefore, class iterator declares iterator(cell *p) as
private, and grants friendship to class queue<T> so that queue<T>’s members begin
and end (and they alone) can access that constructor.

The definition for queue<T>::begin looks like:

template <typename T>


inline
queue<T>::iterator queue<T>::begin() const
{
return first;
}
begin returns an iterator that “points” to the first cell in the queue. Specifically, begin
returns first, which has type cell *, even though begin’s return type is itera-
tor. The return expression applies the private iterator(cell *) constructor to
convert first to an iterator.

queue<T>::end is very similar to queue<T>::begin. The definition for


queue<T>::end looks like:

template <typename T>


inline
queue<T>::iterator
queue<T>::end() const
{
return NULL;
}

Here, now, is an implementation of operator<< that puts the elements in a


queue<string> to an ostream using queue<string>::iterator:

ostream &operator<<(ostream &os, queue<string> &q)


{
queue<string>::iterator i = q.begin();
for (; i != q.end(); ++i)
os << ' ' << *i;
return os;
}

FINAL REMARKS

The template syntax, though occasionally puzzling, is not very difficult to master. Turn-
ing simple non-template functions and classes into templates is fairly easy. However,
writing truly general-purpose templates takes considerable practice.

Templates are easy to use in that template invocations are pretty terse. However, they’re
not so easy when you make a mistake, because your compiler may not give you meaning-
ful diagnostics.

Even though there is a C++ standard, the C++ standards committee will probably be
ironing out problems in templates for years to come. Compiler vendors are scrambling to
catch up to the specification, and finding more problems as they do. For the foreseeable
future, writing portable code using templates will remain difficult.

You might also like