course of C++ programming language
lecture 14, 15: templates
Introduction
Templates provide direct support for generic programming - programming using types as parameters. The C++ template mechanism allows a type to be a parameter in the definition of a class or a function. A template depends only on the properties that it actually uses from its parameter types and does not require different types used as arguments to be explicitly related. In particular, the argument types used for a template need not be from a single inheritance hierarchy.
Using templates, it is possible to create generic functions and classes. In a generic function or class, the type of data upon which the function or class operates is specified as a parameter. Thus, you can use one function or class with several different types of data without having to explicitly recode specific versions for each data type.
Generic functions
A generic function defines a general set of operations that will be applied to various types of data. The type of data that the function will operate upon is passed to it as a parameter. Through a generic function, a single general procedure can be applied to a wide range of data.
By creating a generic function, you can define the nature of the algorithm, independent of any data. Once you have done this, the compiler will automatically generate the correct code for the type of data that is actually used when you execute the function. In essence, when you create a generic function you are creating a function that can automatically overload itself.
A generic function is created using the keyword template
.
The normal meaning of the word template accurately reflects its use
in C++.
It is used to create a template (or framework) that describes what a function
will do, leaving it to the compiler to fill in the details as needed.
Here, X
is a placeholder name for a data type used by the function.
This name may be used within the function definition.
However, it is only a placeholder that the compiler will automatically replace
with an actual data type when it creates a specific version of the function.
Although the use of the keyword class
to specify a generic type in
a template
declaration is traditional, you may also use the
keyword typename
.
The line template <class X> void swapargs (X &a, X &b)
tells
the compiler two things: that a template is being created and that a generic
definition is beginning.
Here, X
is a generic type that is used as a placeholder.
After the template
portion, the function swapargs()
is declared, using X
as the data type of the values that will be
swapped.
In main()
, the swapargs()
function is called using
three different types of data: int
s, double
s, and
string
s.
Because swapargs()
is a generic function, the compiler
automatically creates three versions of swapargs()
: one that will
exchange integer values, one that will exchange floating-point values, and one
that will swap sequences of characters.
A generic function (that is, a function definition preceded by
a template
statement) is also called a template function.
Both terms will be used interchangeably in this book.
When the compiler creates a
specific version of this function, it is said to have created
a specialization.
This is also called a generated function.
The act of generating a function is referred to as instantiating it.
Put differently, a generated function is a specific instance of a template
function.
You can define more than one generic data type in the template
statement by using a comma-separated list.
When you create a template function, you are, in essence, allowing the compiler to generate as many different versions of that function as are necessary for handling the various ways that your program calls the function.
Explicitly overloading a generic function
Even though a generic function overloads itself as needed, you can explicitly overload one, too. This is formally called explicit specialization. If you overload a generic function, that overloaded function overrides (or hides) the generic function relative to that specific version.
Recently, a new-style syntax was introduced to denote the explicit
specialization of a function.
This new method uses the template
keyword.
For example, using the new-style specialization syntax, the overloaded
swapargs()
function from the preceding program looks like this.
Explicit specialization of a template allows you to tailor a version of a generic function to accommodate a unique situation-perhaps to take advantage of some performance boost that applies to only one type of data, for example. However, as a general rule, if you need to have different versions of a function for different data types, you should use overloaded functions rather than templates.
Overloading a function template
In addition to creating explicit, overloaded versions of a generic function,
you can also overload the template
specification itself.
To do so, simply create another version of the template that differs from any
others in its parameter list.
You can mix standard parameters with generic type parameters in a template function. These nongeneric parameters work just like they do with any other function.
The mixing of generic and nongeneric parameters causes no trouble and is, indeed, both common and useful.
Default template parameters
You can give template parameters default values.
For example, you might want to say that the default container for your
Set
is a std::vector
.
The template class definition would look like this.
You can use the type Type
from the first template parameter as the
argument to the std::vector
template in the default value for the
second template parameter.
C++ syntax requires that you do not repeat the default value in the template header line for method definitions.
With this default parameter, clients can now instantiate a grid with or without specifying an underlying container.
Source code organization
Even if you create non-inline function definitions, you'll usually want to put
all declarations and definitions for a template into a header file.
This may seem to violate the normal header file rule of "Don't put in
anything that allocates storage" (which prevents multiple definition
errors at link time), but template definitions are special.
Anything preceded by template<>
means the compiler won't
allocate storage for it at that point, but will instead wait until it's told
to (by a template instantiation), and that somewhere in the compiler and
linker there's a mechanism for removing multiple definitions of an identical
template.
So you'll almost always put the entire template declaration and definition in
the header file, for ease of use.
Consider a simple template:
We can include msg.cpp
wherever msg()
is needed.
For example:
That is, the definition of msg()
and all declarations it depends
on are included in several different compilation units.
It is up to the compiler to generate code when needed (only) and to optimize
the process of reading redundant definitions.
This strategy treats template functions the same way as inline functions.
One obvious problem with this is that everything on which the definition of
msg()
depends is added to each file using msg()
,
thus increasing the amount of information that the compiler must process.
Another problem is that users may accidentally come to depend on declarations
included only for the benefit of the definition of msg()
.
This danger can be minimized by using namespaces, by avoiding macros, and
generally by reducing the amount of information included.
The separate compilation strategy is the logical conclusion of this line of
thinking: if the template definition isn't included in the user code, none of
its dependencies can affect that code.
Thus we split the original msg.cpp
into two files:
The file msg.cpp
now holds all of the information needed to define
msg()
, and msg.h
holds only what is needed to call
it.
A user includes only the declaration (the interface):
This strategy treats template functions the same way it does non-inline
functions.
The definition (in msg.cpp
) is compiled separately, and it is up
to the implementation to find the definition of msg()
when needed.
This strategy also puts a burden on the implementation.
Instead of having to filter out redundant copies of a template definition, the
implementation must find the unique definition when needed.
Note that to be accessible from other compilation units, a template definition
must be explicitly declared export
.
This can be done by adding export
to the definition or to
a preceding declaration.
Otherwise, the definition must be in scope wherever the template is used.
Generic classes
In addition to generic functions, you can also define a generic class. When you do this, you create a class that defines all the algorithms used by that class; however, the actual type of the data being manipulated will be specified as a parameter when objects of that class are created.
Generic classes are useful when a class uses logic that can be generalized. For example, the same algorithms that maintain a queue of integers will also work for a queue of characters, and the same mechanism that maintains a linked list of mailing addresses will also maintain a linked list of auto part information. When you create a generic class, it can perform the operation you define, such as maintaining a queue or a linked list, for any type of data. The compiler will automatically generate the correct type of object, based upon the type you specify when the object is created.
In the following program, the Stack
class is a generic class.
Thus, it can be used to store objects of any type.
In this example, a character stack and a floating-point stack are created, but
any data type can be used.
As you can see, the declaration of a generic class is similar to that of a generic function. The actual type of data stored by the stack is generic in the class declaration. It is not until an object of the stack is declared that the actual data type is determined.
When a specific instance of Stack
is declared, the compiler
automatically generates all the functions and variables necessary for handling
the actual data.
In this example, two different types of stacks are declared:
Stack<char>
and Stack<double>
.
Template parameters
A template can take type parameters, parameters of ordinary types such as
int
and template parameters.
Naturally, a template can take several parameters.
Non-type parameters are restricted to integers, pointers, or references.
Other types, such as float
, are not allowed.
The arguments that you pass to a non-type parameter must consist of either an
integer constant, or a pointer or reference to a global function or object.
Thus, non-type parameters should themselves be thought of as constants, since
their values cannot be changed.
Using default arguments with template classes
A template class can have a default argument associated with a generic type.
It is also permissible for non-type arguments to take default arguments. The default value is used when no explicit value is specified when the class is instantiated. Default arguments for non-type parameters are specified using the same syntax as default arguments for function parameters.
Explicit class specializations
As with template functions, you can create an explicit specialization of
a generic class.
To do so, use the template<>
construct, which works the same
as it does for explicit function specializations.
One specialization is more specialized than another if every argument list that matches its specialization pattern also matches the other, but not vice versa.
Every type can be used as a template argument for the most general
MyVector
, but only pointers can be used for
MyVector<T*>
and only void*
s can be used for
MyVector<void*>
.
References
-
B.Stroustrup: The C++ programming language. Third edition.
Section 13: templates; pp. 327-352. -
H. Schildt: C++: the complete reference. Third edition.
Section 18: templates; pp. 461-487. -
B.Eckel: Thinking in C++. Second edition.
Section 16: introduction to templates; pp. 689-743.