Estimated Reading Time: 45 Minutes
Templates
Templates are a way of writing generic code which can be re-used with different types. This is similar to the polymorphism that we have seen previously through class inheritance, except that the typing for a template happens at compile time rather than runtime.
Templates in C++ come in two main kinds:
- Function Templates
- Class Templates
When a class or function template is used to instantiate an concrete class or function using a specific type, a new class or function definition is created for each type with which the template is instantiated. So unlike our inheritance based run-time polymorphism, where we have one function which can take multiple classes with overlapping definitions (defined by inheritance from a base class), now we use one piece of code to generate multiple separate functions (or classes), each of which accepts a different type. This is why the compiler must know all the types which are to be used in templated functions at compile time. This is sometimes known as “static polymorphism”.
Using Templates with Classes
You have already been using templates in this course: vector<>
is an example of a class template. The vector
class template is defined the type of the elements of the vector left as a template argument: you provide this argument in the angle brackets (<>
) when you declare your vector object. This then tells the compiler what kind of data your vector will hold, so that it can properly handle it (allocating memory, iterating through elements, type checking for elements which are used in the code etc.).
N.B. A note on terminology:
- A class template is not a class, it is a template which can be used to generate the definition of a class once the template arguments have been filled. For example,
vector<>
is a class template which can be used to generate vector classes which hold different types of data, but you cannot create a vector object with no type in the angle brackets. - A template class is a class that has been created from a template. For example
vector<int>
is a template class that has been generated from thevector
class template by providing the typeint
as a template argument.vector<int>
is an entirely separate class fromvector<double>
.
The other containers in the standard library are also class templates, which require you to provide template arguments to instantiate concrete classes. Note that some may take more than one template argument, for example map<>
takes two types: one for the keys and one for the values. Type for a map from strings to integers would be map<string, int>
.
We can define a class template using the following syntax:
template<typename T>
class myClassTemplate
{
public:
myClassTemplate(T value)
{
myMemberT = value;
}
private:
T myMemberT;
};
T
is the template parameter, and thetypename
keyword tells us thatT
must denote a type. (You can equivalently use theclass
keyword.)- Do note that you don’t need to call your template parameter
T
; like function parameters or other variables, it can have any name. It’s good to give it a more meaningful name if the type should represent something in particular, for examplematrixType
could be the name if your templated code deals with arbitrary types representing matrices. This is especially useful when using templates with multiple template parameters!
- Do note that you don’t need to call your template parameter
- We can then use
T
like any other type inside the body of the class definition. - Additional template parameters can appear in the angle brackets in a comma separated list e.g.
template<typename T1, typename T2>
. This is how e.g.std::map
works.
Template parameters do not have to be typename
, i.e. we are not limited to simply templating types. You can also have template parameters that are values such as an int
, or a bool
, or any other type. These can be used to define special versions of classes with separate implementations when provided with particular values. For example we might have template<int maxSize>
to define different classes depending on the maximum size of data it will accept.
std::array
is a good example of a class template which take both a type and a value: the type of the elements and the number of elements in the array.- Having values as template parameters means that they must be constants known at compile time.
- Using template parameters which are values can allow you to leverage to type system to enforce correctness on your program. For example, if your program models objects in 3D space, then you will need a representation of a 3-vector. If you use
std::vector<double>
then these vectors could be any size, so you have to make sure manually that no vectors of other sizes can sneak into your program. If you usestd::array<double, 3>
to represent a 3-vector then the compile will enforce that all positions, velocities, and so on are 3 dimensional. (If you work in general relativity, then this can also help you define different types for 3-vectors (std::array<double, 3>
) and 4-vectors (std::array<double, 4>
)!)
N.B. Templates which have many parameters (types or values) can make type names quite long, so if there is something that you want to use frequently you may consider giving it an alias using the using
syntax:
using Vec3 = std::array<double, 3>;
This can also make your type names more meaningful to people reading your code.
Template Classes and Inheritance
Consider a class template which takes one type as a template argument, such as vector
.
Let us say we have two classes A
and B
, where B
is a sub-class of A
. It is important to understand that the template classes vector<A>
and vector<B>
do not share the same relationship as A
and B
i.e. vector<B>
is not a sub-class of (does not inherit from) vector<A>
.
Function Templates
As well as creating templates for classes we can also create templates for functions when we want to use the same code to describe the behaviour of a function taking a variety of different types. In fact, most class template definitions will also contain function templates, since member functions are likely to be dependent on template parameters.
Function templates can have the same kinds of template parameters as class templates.
The syntax for declaring function templates is essentially identical to declaring a class template:
template<typename T>
T templatedAdder(T a, T b)
{
T c = a + b;
return c;
}
- This function can only be created for a given type if the function body forms valid expressions when the type is substituted for
T
.- In this case the restriction is that the operators
=
and+
must be defined for typeT
. - This applies to integers, double, strings etc. (see below on Operator Overloading for more information).
- In this case the restriction is that the operators
A common example of a templated function would be a function which acts on a container type but doesn’t need to access the data itself. Consider this example which take every other element of a vector:
template<typename T>
vector<T> everyOther(vector<T> &v_in)
{
vector<int> v_out;
for(size_t i = 0; i < v_in.size(); i++)
{
if(i % 2 == 0) v_out.push_back(v_in[i]);
}
}
- The exact details of the type
T
don’t matter in this case, since we never access the data of typeT
anyway. The only restriction onT
is that it can be added to a vector. - A function can be generated for every kind of vector in this way.
Using Templates with Overloaded Functions
One very useful way to make use of templates is to exploit operator / function overloading. Operators or functions which are “overloaded” can operate on multiple types, for example:
- The arithmetic operators
+
,-
,*
, and/
are defined for a variety of types includingint
,float
, anddouble
. It’s often easy to write numerical functions which operate on generic numerical types using templates. ThetemplatedAdder
example above will work on any C++ type for which+
is well defined. - The
[]
operator can be used to access many kinds of data-structures includingvector
,array
,map
, and C-style arrays. We can therefore write functions which are agnostic about the precise kind of storage used, as long as the same code will work for all of the types that we are interested in. - The pointer dereferencing operator
*
works on unique, shared, and raw pointers, so we can write template code which can be used with any of these types if we don’t use functionality that is specific to one or the other of them.
When we are designing classes we can overload operators ourselves. This can be very important when defining types that we want to be able to use within templated functions. Consider for example a fraction type:
class Fraction
{
public:
Fraction(int a, int b) : numerator(a), denominator(b) {}
private:
int numerator;
int denominator;
};
- This class represents a rational number: a ratio of two integers.
- This is an appropriate arithmetic type, so it would make sense for us to define operators like
+
,-
,*
, and/
for the Fraction type.- This would allow us to pass it to templated functions that deal with generic arithmetic types.
- This can therefore be more flexible and intuitive than defining member functions for these kinds of operations.
Let’s define the *
(multiplication) operator. (The others can be defined similarly.) We can do this in one of two ways.
A) As a member function:
class Fraction
{
public:
Fraction(int a, int b) : numerator(a), denominator(b) {}
Fraction operator* (Fraction y)
{
return Fraction(numerator * y.numerator, denominator * y.denominator);
}
private:
int numerator;
int denominator;
};
- The member function
operator*
can be called like any other member function.- If you have two
Fraction
objectsf1
andf2
you could calculate a new fractionFraction f3 = f1.operator*(f2);
.
- If you have two
- However this function also overloads the
*
infix operator.- So we can now write
Fraction f3 = f1 * f2;
.
- So we can now write
- Note that because it is a member function, it has access to the private member variables
numerator
anddenominator
.
B) Outside the class
class Fraction
{
public:
Fraction(int a, int b) : numerator(a), denominator(b) {}
int getNumerator(){ return numerator; }
int getDenominator(){ return denominator; }
private:
int numerator;
int denominator;
};
Fraction operator*(Fraction x, Fraction y)
{
int numerator = x.getNumerator() * y.getNumerator();
int denominator = x.getDenominator() * y.getDenominator();
return Fraction(numerator, denominator);
}
- This version also overloads the
*
infix operator in the same way. - There is now no member version so we can’t call
f1.operator*(f2)
, but we can call the non-member functionoperator*(f1, f2)
. - Note that we had to create
get
functions for the private member variables becauseoperator*
is not a member function in this case and so does not have access to private members.
Using Class Members with Templated Types
Writing a templated function which takes type T
implicitly defines an interface that must be met by T
.
- In the simple adding example we use the
+
operator on objects of typeT
: thereforeT
must implement this operator in order for the function to be successfully generated. - In our vector sampling example we only require that
vector<T>
is a valid type; this places very few restrictions onT
(although there are some).
The only restrictions on our templates is that the code is valid once the type substitution has been made. If we have a template parameter T
then we can access member variables and functions of the class T
, and this function will be able to be generated for any class which implements those properties. For example, let’s take this function:
template<typename T>
T& getTheBiggerOne(T &a, T &b)
{
if(a.getArea() >= b.getArea())
{
return a;
}
else
{
return b;
}
}
- This function will return whichever of its two inputs has the larger area.
- This is a valid template for any type which implements the member function
getArea()
. - Note we don’t specify the return type of
getArea()
; for this code to be valid it is only necessary that>=
is defined for the return type ofgetArea()
.
We can call this function using our Shape
classes (Shape
, Circle
, Square
) from last week, since they implement getArea()
. But we could also implement a new class, like this one:
class Country
{
Country(string n, double a, double p) : name(n), area(a), population(p) {}
double getName() { return name; }
double getArea() { return area; }
double getPopulation() { return population; }
private:
string name;
double area;
int population;
}
- This class defines countries by their name, area, and population.
- This class also fulfills all the conditions of
getTheBiggerOne
:getArea()
is implemented.>=
is defined fordouble
.
We can use getTheBiggerOne
with our Country
class just as well as our Shape
class, even though they are not (and certainly should not be) related by any inheritance! Templates allow us to define generic code that is broader than inheritance based polymorphism, on the condition that the type can be determined at compile time in order to generate a statically typed function.
- Class inheritance provides run-time polymorphism: I can define one function that takes a base class (e.g.
Shape
) and objects of that class or its derived classes can be passed to it. The compiler does not need to know at compile time whether the function will end up receivingShape
orCircle
orSquare
. - Templates provide static polymorphism. I can define one function template that generates separate functions for each class. If I want to use my function with both
Shape
andCountry
, the compiler needs to know this at run time.- I can’t declare a single function or class (such as a container), which can take both
Shape
andCountry
. For example, I can’t put aShape
object in the same vector as aCountry
object, since it either needs to be avector<Shape>
orvector<Country>
. - If I use the function with
Shape
and withCountry
in the same program, I will actually generate two functions:Shape& getTheBiggerOne(Shape&, Shape&)
andCountry& getTheBiggerOne(Country&, Country&)
. These functions are separate because they have different signatures (parameter and return types).
- I can’t declare a single function or class (such as a container), which can take both
- These two can be combined. For example,
getTheBiggerOne
is a template which could be instantiated with the typeShape
. The resulting function, which takes and returns references toShape
, could be used with objects of typeShape
,Circle
orSquare
(run time polymorphism based on their inheritance tree) but notCountry
(this is not part of the same inheritance tree).
Organising and Compiling Code with Templates
Since templates create concrete classes and functions at compile time, the compiler needs to have access to the template definition and the argument(s) for which it needs to be instantiated together at compile time.
Well organised C++ code typically organises code into declarations and implementations:
- Declarations are contained in header files, usually ending in
.h
or.hpp
. (Sometimes.h
is used to distinguish header files intended for use with C and.hpp
for header files which are only compatible with C++, but this is just a convention and not uniformly applied.) These are typically there to declare classes, and the variables and functions that they possess, and/or the variables and functions contained in a particular namespace. Function declarations just contain the name and signature (input types and return type) of the function, but not the actual code that it executes. When writing code which uses a class we usually just include the header file in our code to include the declaration: as long as we know the interface for the class we can compile code down to an object file which interfaces with that class. The final code can be created by linking with the class object file which defines the actual implementation that needs to be executed. - Implementations are contained in source files, usually ending in
.cpp
for C++ files. These contain the actual code which is executed by the functions declared in the header. (There may also be functions declared in the source file which aren’t in the header if they’re only locally needed and therefore don’t need to be included elsewhere.) This can then be compiled down to an object file for linking with other object files which interface with each other.
We have to be a little careful when working with this model in the case of templates. We’ll explore this using a function template, although the considerations are the same for a class template as well. Let’s declare a function in a header file, and write two source files: one which implements the function in the header, and one which makes use of this function.
First the declaration in declaration.hpp
:
#ifndef DECLARATION_HPP
#define DECLARATION_HPP
namespace utilFunctions
{
int add(int, int);
}
#endif
- It’s usually a good idea to put functions and variables which are defined in a header but not part of a class inside a namespace, to avoid potential name clashes.
- The
#ifndef
,#define
and#endif
are pre-processor directives. This pattern is called an “include guard”: it means that the file’s contents will be ignored if it has already been included somewhere else in the same compilation unit, so that the contents are not declared twice (which would cause an error).- An alternative to these include guards which you will have seen already is
#pragma once
. This is a common pre-processor directive to only include a file once in a compilation unit, but it is not part of the ISO C++ standard and therefore may not be compatible with all platforms and compilers.
- An alternative to these include guards which you will have seen already is
Then the implementation in implementation.hpp
:
#include "declaration.hpp"
int utilFunctions::add(int a, int b)
{
int c = a + b;
return c;
}
- We need to include
declaration.hpp
in order to have access to the namespace and function declaration.
Finally we have another file which will want to use this function, which we’ll call usage.cpp
.
#include "declaration.hpp"
#include <iostream>
int main()
{
int x = 15;
int y = 27;
std::cout << utilFunctions::add(x, y) << std::endl;
return 0;
}
- This also needs to include
declaration.hpp
in order to access the function declaration. - Note that it does not need the function implementation: it only needs to know the name of the function and what types it accepts and returns to be able to compile.
We can compile implementation.cpp
and usage.cpp
down to two separate object files and link them to produce a fully functional executable.
This simple addition function could be made much more flexible by also operating on other numeric types, like float
or double
for which the addition operator is defined. In this case, rather than writing three separate functions out, all with essentially identical code, we could use a function template.
Let’s update our declaration to use a template:
#ifndef DECLARATION_HPP
#define DECLARATION_HPP
namespace utilFunctions
{
template<typename T>
T add(T, T);
}
#endif
and our implementation:
#include "declaration.hpp"
template<typename T>
T utilFunctions::add(T a, T b)
{
T c = a + b;
return c;
}
If we try now to compile and link our executable we will find an error like this:
undefined reference to `int utilFunctions::add<int>(int, int)'
- The compiler has been unable to implement a definition of the
add
function for the typeint
, so this definition does not exist for us to use. - This error shows up during linking. You can compile both object files like before, because both match the template declaration and therefore are valid, but neither one can define the specific implementation that we want so when linking it finds that the function isn’t defined anywhere.
implementation.cpp
cannot define the implementation when compiled down to an object because it has the function template but not the intended type, so it can’t come up with any concrete implementation.usage.cpp
cannot define the implementation when compiled down to an object because it knows what type it should be used for, but it doesn’t have the templated implementation (this is inimplementation.cpp
, and we have only includeddeclaration.hpp
).
There are two possible ways to approach this problem.
- We can include the templated function implementation in the header file instead of a separate source file.
- In this case the compiler can use the template to create the function for whatever type is called for in
usage.cpp
. - Concrete function is only created from the template if it is actually used.
- This is flexible, but breaks the separation of declaration and implementation.
- Can cause the size of the executable to increase because the definitions will be recreated in different compilation units.
- In this case the compiler can use the template to create the function for whatever type is called for in
#ifndef DECLARATION_HPP
#define DECLARATION_HPP
namespace utilFunctions
{
template<typename T>
T add(T a, T b)
{
T c = a + b;
return c;
}
}
#endif
- We can keep our header file with just the declaration, and tell the compiler which types to implement the function for in the source file (
implementation.cpp
).- In this case,
usage.cpp
will only be able to useadd
for the types which are explicitly instantiated inimplementation.cpp
. - This is less flexible as you need to anticipate any combination of template arguments that the function will be used with, but keeps the declaration and the implementation separate.
- Separate function implementations will be created for each set of types given, even if they are never used.
- It can also be useful if you want the function to restrict usage to a sub-set of possible types.
- In this case,
#include "declaration.hpp"
template<typename T>
T utilFunctions::add(T a, T b)
{
T c = a + b;
return c;
}
template int utilFunctions::add(int, int);
template float utilFunctions::add(float, float);
template double utilFunctions::add(double, double);