Anda di halaman 1dari 16

Introduction to Templates in C++

Class 406
Dan Saks
INTRODUCTION
This paper introduces templates, the C++ facility for implementing generic classes and
functions. It explains the basic capabilities of function templates and class templates. It
also explains template compilation mechanisms including instantiation, specialization,
and argument deduction.
This is a discussion of general-purpose C++ programming features and techniques, which
are useful in, but not specific to, embedded systems.
FUNCTION TEMPLATES
Here are two overloaded functions named swap:
void swap(int &a, int &b);
void swap(string &a, string &b);
Each function swaps the value of its first argument with the value of its second argument.
For example,
int i, j;
...
swap(i, j); // calls swap(int &, int &)
leaves the value that was in i in j, and the value that was in j in i. Similarly,
string s, t;
...
swap(s, t); // calls swap(string &, string &)
leaves the value that was in s in t, and the value that was in t in s.
Not surprisingly, the definitions for these two swap functions are remarkably similar to
each other:
void swap(int &a, int &b)
{
int temp = a;
a = b;
b = temp;
}
void swap(string &a, string &b)
{
string temp = a;
a = b;
b = temp;
}
In fact, the functions are identical except that the second refers to string whenever the
first refers to int. If you define other swap functions, they will surely follow this pat-
tern.
Overloading lets you use the same name for functions that perform the same operation,
regardless of the operand types. When used in moderation, overloading function names
produces libraries that are easier to use than they would be without overloading. How-
ever, overloading does not spare the effort of writing a nearly identical code for each
nearly identical function. Function templates do.
A function template is a generalized description of an algorithm. It is not an actual func-
tion. Rather, it is a single definition that the compiler can use to generate an unbounded
set of overloaded functions.
For example, you can replace the declarations for all the swap functions with a single
function template declaration:
template <typename T>
void swap(T &a, T &b);
The corresponding function template definition is:
template <typename T>
void swap(T &a, T &b)
{
T temp = a;
a = b;
b = temp;
}
In both the template declaration and definition, <typename T> is a template parameter
list, which specifies T as a template type parameter.
The keyword typename in the template parameter list indicates that T is a placeholder
for any type. A type parameter can be replaced by a non-class type (such as int or
char *) or by a class type (such as string). C++ allows the keyword class as an
alternative to the keyword typename in template parameter lists (but only in template
parameter lists). For example,
template <class T>
void swap(T &a, T &b);
Although the keyword class suggests that T must be a class type, T can still be replace
by any type.
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
Its difficult to explain how templates work without using the terms instantiation and
specialization. Unfortunately, the C++ Standard is not very clear about what these words
mean. Consequently, even experts attribute different meanings to these words and use
them inconsistently. Heres an attempt to define them consistently.
A function template is a generalization of an algorithm that can be tailored to work with
different compile-time arguments. For example, swap<T> is a general function that
swaps the values of two objects of the same type T. T can be any of a wide variety of
types.
A specialization of a template is the adaptation of that template to a specific compile-time
argument. For example, swap<string> is the name of the function that is the spe-
cialization of the swap template for type argument string. swap<char const *>
is the name of the function that is the specialization of the swap template for type argu-
ment char const *.
A program can call a function template specialization simply by using its name as the
function name in a function call. For example,
string s, t;
...
swap<string>(s, t);
or:
char const *p, *q;
...
swap<char const>(p, q);
swap<string> and swap<char const *> are examples of a construct called a
template-id. A template-id has the form:
template-name<type-id>
A type-id is the same construct for describing types that can appear in a cast expression:
(type-id)expression
or a sizeof expression:
sizeof(type-id)
C++ compilers generate definitions from templates as needed to satisfy references to the
names of template specializations. Instantiation is the process of generating a definition
from a template.
For example, when it encounters swap<string> as the function name in a function
call, the compiler uses the template swap<T> to instantiate a definition for:
void swap<string>(string &a, string &b);
to satisfy the call.
Instantiation has an another meaning: an instantiation is a definition generated from a
template. An instantiation is one form the most common form for a specialization.
CLASS TEMPLATES
A class template is a single class definition that can be tailored to define similar, yet dis-
tinct, types. A class template can take the place of many classes with similar names and
nearly identical definitions.
For example, class rational represents a rational number (an exact fraction) as a pair
of long, which is short for signed long int. The class definition looks like:
class rational
{
public:
rational();
rational(long n);
rational(long n, long d);
...
private:
long num, denom;
};
Not all applications that perform rational arithmetic need such precision. You can pro-
vide a more flexible library by renaming class rational as long_rational (for
long precision), and defining new classes rational (for int precision), and
short_rational (for short precision). You might even want to define unsigned
versions for each of those classes. Thats at least six distinct classes you must write.
Rather than write all these similar classes, you can write a single class template, declared
as:
template <typename T>
class rational;
and defined as:
template <typename T>
class rational
{
public:
rational();
rational(T n);
rational(T n, T d);
...
private:
T num, denom;
};
Hence, rational<long> is a template-id, but this template-id names a class type
rather than a function. In particular, its the name of the class that is the specialization of
the rational template for the type argument long. Conceptually, its the type name
for a rational number with long precision.
You can use a template-id that names a class template anywhere you can use any other
class name. For example,
rational<long> rl (10);
declares rl as an object of type rational<long> with initial value 10/1. Also,
typedef rational<int> rat;
defines rat as an alias for the type rational<int>, so that:
rat r (4);
declares r as an object of type rational<int> with initial value 4/1. Finally,
rational<unsigned short> *p;
defines p as an object of type pointer to rational<unsigned short>.
MEMBERS OF CLASS TEMPLATES
As with non-template classes, definitions for member functions of a class template can
appear inside the class definition, as in:
template <typename T>
class rational
{
public:
rational(T n):
num(n), denom(1)
{
}
...
private:
T num, denom;
};
or they can appear outside the class definition, as in:
template <typename T>
rational<T>::rational(T n):
num(n), denom(1)
{
}
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 n):
num(n), denom(1)
{
}
the full-qualified function name is rational<T>::rational.
Within the scope of class template rational<T>, you can usually refer to the class
name as either rational or rational<T>. For example, you can write the class
definition for rational<T> as:
template <typename T>
class rational
{
public:
rational();
rational(T n);
rational(T n, T d);
...
rational &operator+=(rational const &ro);
...
};
or as:
template <typename T>
class rational
{
public:
rational<T>();
rational<T>(T n);
rational<T>(T n, T d);
...
rational<T> &operator+=(rational<T> const &ro);
...
};
A name appearing to the right of rational<T>:: is also in the scope of ra-
tional<T>. Thus you can write the definition for rational<T>::operator+=
using either:
template <typename T>
rational<T> &rational<T>::operator+=(rational const &ro)
{
...
} reenter the class scope here
or:
template <typename T>
rational<T> &rational<T>::operator+=(rational<T> const &ro)
{
...
}
The exception to this rule is that you cannot write rational<T> immediately after ::.
For example, when it appears outside the class template definition, the definition for ra-
tional<T>s converting constructor must look like:
template <typename T>
rational<T>::rational(long n):
num(n), denom(1)
{
}
but not like:
template <typename T>
rational<T>::rational<T>(long n): // error
num(n), denom(1)
{
}
A class template can have static data members. As with non-template classes, the defini-
tion for a static data must appear outside its class definition. For example:
template <typename T>
class gadget
{
...
static unsigned long counter;
};
declares counter as a static data member of gadget<T>. The definition for that
counter looks like:
template <typename T>
unsigned long gadget<T>::counter = 0;
A class template can have members that are types. For example, many container classes
in the Standard C++ library have a member type called iterator, declared as in:
template <typename T>
class list
{
public:
class iterator; // member type declaration
...
};
Outside the class template definition, you refer to the iterator type by its fully quali-
fied name. For example,
list<string>::iterator i;
declares i as an object of type iterator for a list of string.
FRIENDS OF CLASS TEMPLATES
A class template can declare friends, as in:
template <typename T>
class rational
{
friend ostream &operator<<
(ostream &, rational const &);
...
}
Again, most of the time when it appears in the scope of rational<T>, the name ra-
tional by itself is a synonym for rational<T>. Thus, the above friend declaration
for operator<< is actually a declaration for a (non-member) function template with
type parameter T. When it appears outside the class definition, the definition of opera-
tor<< must look like:
template <typename T>
ostream &operator<<(ostream &s, rational<T> const &r)
{
...
}
A function thats 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 gadget
{
friend int foo(int);
...
};
foo is a non-template function that is a friend of every specialization of class
gadget<T>. Its not clear that this is good for anything, but C++ does allow it.
TEMPLATE ARGUMENTS AND PARAMETERS
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<int> > ratios;
declares ratios as a list specialized with elements of type rational<int>. If
you omit the space immediately after rational<int>, you get a compile error:
list<rational<int>> ratios; // no! >> is a shift operator
The compiler interprets >> as a shift operator, not as two closing delimiters.
A template parameter can be an expression rather than a type, 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 can have inte-
ger, enumeration, pointer, or reference type.
A template can have more than one parameter. For example,
template <typename T, typename U>
class pair
{
T first;
U second;
};
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 pair
consisting of a string and a double.
A template can also have default arguments, as in:
template <typename T, typename X = size_t>
class array
{
public:
array(X, X);
...
private:
T *actual_array;
X lo, hi;
};
This example defines a template for an array with elements of type T and indices of
type X. X has size_t as its default, so that:
array<double> a(1, 10);
defines a as an array with elements of type double and indices of type size_t in
the range 1 to 10, inclusive. On the other hand,
array<long, int> b(-100, +100);
defines b as an array with elements of type long and indices of type (plain) int in
the range -100 to +100, inclusive.
COMPILATION MODELS
A C++ translator (compiler and linker) automatically instantiates definitions as needed
for each template specialization. Unfortunately, but not surprisingly, different translators
use different approaches to support automatic instantiation. The differences affect
whether you place definitions in headers or in source files. These differences make it
hard to write libraries of templates for multiple platforms.
The two template compilation models are the separation model and the inclusion model:
Under the separation model:
For class templates, you place the class definition and inline member function defini-
tions in a header (as you do with non-templates). You place all other class member
function definitions and static data member definitions in one or more corresponding
source files.
For non-member function templates, you place inline function definitions and non-
inline function declarations in a header (as you do with non-templates). You place all
non-inline function definitions in one or more corresponding source files.
Under the inclusion model:
You place all template declarations and definitions in a header. You do not place
anything in source files.
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 standardization process. It may be years
before most compilers actually do what the standard says.
TEMPLATE ARGUMENT DEDUCTION
In many situations, compilers can deduce template argument(s) from the argument(s) to a
call that names a function template. In such cases, the call need not specify the template
argument explicitly. That is, the call can use just the template name rather than a tem-
plate-id as the function name.
For example, given the function template:
template <typename T>
T Abs(T x)
{
return x < 0 ? x : x;
}
and:
int i;
float f;
a program can call:
i = Abs(i);
The compiler can deduce that this is equivalent to:
i = Abs<int>(i);
Similarly, a compiler can deduce that:
f = Abs(f); // calls Abs<float>(f)
requires a specialization for Abs<float>.
Compilers can deduce template type parameter(s) from function parameter(s) with forms
more complicated than just T. For example, in:
template <typename T>
void swap(T &a, T &b)
{
T temp = a;
a = b;
b = temp;
}
the template has type parameter T, but the function parameters have type T &. Given:
int i, j;
string s, t;
float *p, *q;
compilers should still be able to deduce:
swap(i, j); // calls swap<int>(i, j)
swap(s, t); // calls swap<string>(s, t)
swap(p, q); // calls swap<float *>(p, q)
The C++ standard says that compilers should be able to deduce a template type argument
T even when its buried deep inside a complicated type. For example, given:
template <typename T>
void f(T const *a[]);
and:
char const *names[] =
{ "Moe", "Larry", "Curly", NULL };
a compiler should be able to deduce that the template type argument used in:
f(names); // calls f<char>(names)
is char.
A function template can have a function parameter composed from a template-id. For
example,
template <typename T>
void sort(vector<T> &t)
{
...
T temp;
...
}
is a template for a function that will sort a vector specialized for any type T. Compilers
should be able to deduce T from the actual arguments to a call. For example,
vector<string> names;
...
sort(names); // calls Sort<string>(names)
Unfortunately, compilers vary in their deductive capabilities. Few, if any, can deduce
arguments composed from all of the forms that the C++ standard requires.
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 arithmetic types. More specifically, 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.
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, theyre
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.

Anda mungkin juga menyukai