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_type is the type of the return value.
  • function_name is 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 compiler

Function 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 for Point objects.
  • we can specialize the mean function for Point objects.

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 Point objects, 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>;
  1. When compiling matrix.cpp, the compiler will instantiate the templates for int and double.
  2. When compiling main.cpp, compiler assumes Matrix<int> and Matrix<double> exists somewhere.
  3. Finally the linker will link the object files together.

If there’s no explicit instantiation in matrix.cpp,

  1. When compiling matrix.cpp, since template is just a blueprint, the compiler will not instantiate anything.
  2. When compiling main.cpp, compiler still assumes Matrix<int> and Matrix<double> exists somewhere.
  3. During linking, since matrix.cpp does 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 ints

Example: 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 of T with length length.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.

  • bool is a special type, it only has two values: true(1) and false(0).
  • We can save memory by storing bool values using a bit instead of a byte.

Before implementing the specialization, let’s take a gentle look at the bitwise operations.

<< and >> operations

  • a << b shifts the bits of a to the left by b positions.
    • Inserting b 0s at the end of a.
    • Equivalent to multiplying a by 2^b.
  • a >> b shifts the bits of a to the right by b positions.
    • Removing the last b bits from a.
    • Equivalent to dividing a by 2^b and discarding the remainder.

On the diagram above, green color represents the bits that are inserted, red color represents the bits that are removed.

  • 5 >> 1 is 2 because 5 is 101 in binary, shifting right by 1 position gives 10, which is 2 in decimal.
  • 5 << 1 is 10 because 5 is 101 in binary, shifting left by 1 position gives 1010, which is 10 in decimal.

| and & operations

  • a | b performs a bitwise OR operation.
  • a & b performs 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.
  • 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

  1. Sometimes you may see typename replaced by class, but they are essentially the same.↩︎

  2. This syntax is called a reference to an array but is rarely used, see this StackOverflow post for more details.↩︎