Templates
Compile time polymorphism
(Runtime) Polymorphism is a powerful feature in object-oriented programming,
- allows objects of different classes to be treated as if they belong to a common base class
- enables dynamic binding, where the appropriate method is called at runtime based on the actual object type.
(Compile time) Polymorphism achieves similar goals but through compile-time polymorphism, which is achieved using templates.
Function Templates
Motivating Example
Suppose I want to write function to calculate the mean of an array of numbers.
Since we have learned function overloading, we can write the following:
double mean(const double* data, size_t size);
int mean(const int* data, size_t size);
float mean(const float* data, size_t size);
// ...However, during implementation, every single function is almost identical. We are repeating ourselves.
Function Templates
To avoid code duplication, we can use function templates. The syntax is as follows:
template <parameter list>
return_type function_name(parameter list) {
// function body
}<parameter list>is a list of parameters, which can be of any type.return_typeis the type of the return value.function_nameis the name of the function.
For example, in our motivating example, we can write the following1:
template <typename T>
T mean(const T* data, size_t size) {
T sum = 0;
for (size_t i = 0; i < size; ++i) {
sum += data[i];
}
return sum / size;
}Template Instantiation
Template instantiation is the process of creating a class or a function from a template.
- The function template is only a blueprint, it is not a function until it is instantiated.
- Once the compiler instantiates a function from a template, it is a regular function.
- The template parameter must be determined at compile time.
- You can instantiate a template both explicitly and implicitly.
Explicit Instantiation
Based on the mean function template, we can explicitly instantiate a function for specific types.
template double mean<double>(const double*, size_t);
// The following is equivalent, except for the type name
template int mean<>(const int*, size_t);
// The following is still equivalent
template short mean(short*, size_t);Then we are able to call the instantiated functions.
int data[] = {1, 2, 3, 4, 5};
size_t size = 5;
int a = mean<int>(data, size);Implicit Instantiation
Implicit instantiation is when the compiler automatically instantiates a function template for a given type.
- Generally, you don’t have to provide template function parameters.
- The compiler can deduce them from usage.
Without doing explicit instantiation, you can directly use the following:
int data[] = {1, 2, 3, 4, 5};
size_t size = 5;
int a = mean(data, size); // type int is deduced by the compilerFunction template specialization
We still use the mean function as an example, sometimes we want to calculate the mean of an array of Point objects.
struct Point {
int x;
int y;
};- the
operator+is not defined forPointobjects. - we can specialize the
meanfunction forPointobjects.
Specialization example
To specialize the mean function for Point objects, we can write the following:
template <>
Point mean<Point>(const Point* data, size_t size) {
double sum_x = 0;
double sum_y = 0;
for (size_t i = 0; i < size; ++i) {
sum_x += data[i].x;
sum_y += data[i].y;
}
return Point{sum_x / size, sum_y / size};
}- The
template <>syntax is used to specialize a template function. - After specialization, for
Pointobjects, the compiler will use this specialized version of the function.
Class Templates
Motivating Example
Suppose we want to write a Matrix class, the elements can be of different types.
class IntMatrix {
int* data;
size_t rows;
size_t cols;
public:
int get(size_t row, size_t col) const;
void set(size_t row, size_t col, int value);
// ...
};class DoubleMatrix {
double* data;
size_t rows;
size_t cols;
public:
double get(size_t row, size_t col) const;
void set(size_t row, size_t col, double value);
// ...
};Most of the code is the same, we are again repeating ourselves.
Class Templates
Like function templates, class templates are blueprints for classes.
template <typename T>
class Matrix {
T* data;
size_t rows;
size_t cols;
public:
T get(size_t row, size_t col) const;
void set(size_t row, size_t col, T value);
// ...
};You should be cautious when implementing member functions outside the class definition.
File structure for templates
Templates in C++ are typically implemented in header files.
Reason:
- If we want to separate the interface and implementation, we need to put the declaration and definition in different files.
- Since the template is instantiated at compile time, when you put the declaration and definition in different files, you need to explicitly instantiate the template, otherwise the linker will complain.
For example, in matrix.h, you declare the template and implement the member functions in matrix.cpp.
If you want to use Matrix<int> and Matrix<double> in main.cpp, you need to explicitly instantiate them in matrix.cpp:
template class Matrix<int>;
template class Matrix<double>;- When compiling
matrix.cpp, the compiler will instantiate the templates forintanddouble. - When compiling
main.cpp, compiler assumesMatrix<int>andMatrix<double>exists somewhere. - Finally the linker will link the object files together.
If there’s no explicit instantiation in matrix.cpp,
- When compiling
matrix.cpp, since template is just a blueprint, the compiler will not instantiate anything. - When compiling
main.cpp, compiler still assumesMatrix<int>andMatrix<double>exists somewhere. - During linking, since
matrix.cppdoes not have the instantiation, the linker will complain.
Non-type template parameters
Template parameters
A template parameter declared with the typename or class keyword is a type template parameter.
- It stands for some yet-to-be-determined type.
Alternatively, a template parameter can be a non-type template parameter.
- It stands for some yet-to-be-determined value.
- The value must be determined at compile time.
Non-type template parameters can be of multiple types, in this class we only focus on fundamental types and pointer types.
Example: static matrix
We call it a static matrix because the size of the matrix is determined at compile time. (No dynamic memory allocation at runtime.)
template <typename T, size_t rows, size_t cols>
class Mat {
T data[rows][cols];
public:
Mat(); // no need to specify the size of the matrix
T get(size_t row, size_t col) const;
void set(size_t row, size_t col, T value);
};To create Mat instances, we can use
Mat<int, 3, 3> mat; // 3x3 matrix of intsExample: get function
Suppose we want to implement a get function to get the value for an array.
- The array size is determined at compile time.
- The template parameters are also determined at compile time.
We can design a get function using non-type template parameters.
template <typename T, size_t length>
T& get(T (&arr)[length], size_t index) {
if (index >= length) {
throw std::out_of_range("Index out of bounds");
}
return arr[index];
}Here’s a few things to notice:
- Array is passed by reference.
- Array exists after the function call, we return a reference to the element.
- The first argument
T (&arr)[length]is a reference to an array ofTwith lengthlength.2
A simple main function to test the get function:
constexpr size_t length = 5; // constexpr is a compile-time constant
int arr[length] = {1, 2, 3, 4, 5};
std::cout << get<length>(arr, 5) << std::endl;Class template specialization
Example: Matrix class
The previous Matrix class can handle most of the cases, but we want to save the memory for type bool.
boolis a special type, it only has two values:true(1) andfalse(0).- We can save memory by storing
boolvalues using a bit instead of a byte.
Before implementing the specialization, let’s take a gentle look at the bitwise operations.
<< and >> operations
a << bshifts the bits ofato the left bybpositions.- Inserting
b0s at the end ofa. - Equivalent to multiplying
aby2^b.
- Inserting
a >> bshifts the bits ofato the right bybpositions.- Removing the last
bbits froma. - Equivalent to dividing
aby2^band discarding the remainder.
- Removing the last
On the diagram above, green color represents the bits that are inserted, red color represents the bits that are removed.
5 >> 1is2because5is101in binary, shifting right by 1 position gives10, which is2in decimal.5 << 1is10because5is101in binary, shifting left by 1 position gives1010, which is10in decimal.
| and & operations
a | bperforms a bitwise OR operation.a & bperforms a bitwise AND operation.
For example, 5 & 15 is 5, 5 | 15 is 15.
Next we will specialize the Matrix class for bool type.
template <>
class Matrix<bool> {
size_t rows;
size_t cols;
std::shared_ptr<unsigned char> data;
public:
Matrix(size_t rows, size_t cols);
bool get(size_t row, size_t col) const;
void set(size_t row, size_t col, bool value);
};
template <>
Matrix<bool>::Matrix(size_t rows, size_t cols) : rows(rows), cols(cols) {
int num_bytes = (rows * cols + 7) / 8;
data = std::shared_ptr<unsigned char>(new unsigned char[num_bytes]{});
}
template <>
bool Matrix<bool>::get(size_t row, size_t col) const {
size_t idx = row * cols + col;
size_t byte_idx = idx / 8;
size_t bit_idx = idx % 8;
return (data[byte_idx] >> bit_idx) & 1;
}
template <>
void Matrix<bool>::set(size_t row, size_t col, bool value) {
size_t idx = row * cols + col;
size_t byte_idx = idx / 8;
size_t bit_idx = idx % 8;
if (value) {
data[byte_idx] |= 1 << bit_idx;
} else {
data[byte_idx] &= ~(1 << bit_idx);
}
}Summary
- Run-time polymorphism or compile-time polymorphism?
- When you want polymorphism, use templates.
- Sometimes templates can’t be used because the type is deduced at runtime, in this case you can use run-time polymorphism.
- Example: A game works on a grid-based world, the object on a cell can be a
Tank,Wall,Tree, which is not known at compile time.
- Example: A game works on a grid-based world, the object on a cell can be a
- Only a subset of features of template are discussed in this lecture, things like partial specialization, variable template parameters are not covered. You can refer to cppreference for more details.
Footnotes
Sometimes you may see
typenamereplaced byclass, but they are essentially the same.↩︎This syntax is called a reference to an array but is rarely used, see this StackOverflow post for more details.↩︎