C++ Metaprogramming: Introduction to Templates

Introduction to C++ metaprogramming using templates.

Template Constants

Template constants were introduced in C++14. They allow you to create constants that can exist as multiple types and/or precision such as int, float or double versions. Here we use a template to instantiate the different constexpr declarations as needed.

// Template constants
template<typename T>
constexpr T π_v = T(3.14159265358979323846L);

inline constexpr double π = π_v<double>;

The template definition declares one type parameter called T. It then defines the constant expression for π_v to be of type T assigning it the most precise version of pi (long double) needed. Just for convenience we also define an inline constexpr for π using double precision. If this template definition doesn't work for some specific type, we can specialize the template for the desired type. For example lets say that we also want a string representation of the variable. We can specialize the template for type const char * and return a C-style string.

// Specialization to return a string...
template<>
constexpr const char* π_v<const char*> = "π";

For example:

main.cpp
#include <cmath>
#include <iostream>
#include "ParameterPacks.h"

using namespace std;
using namespace ParameterPacks;

// Template constants
template<typename T>
constexpr T π_v = T(3.14159265358979323846L);

inline constexpr double π = π_v<double>;

// Specialization to return a string...
template<>
constexpr const char* π_v<const char*> = "π";

int main(int argc, const char * argv[]) {
    cout << verySimpleFormat("{}<{}>: {}", π_v<const char *>,
            "int", π_v<int>) << endl;
    cout << verySimpleFormat("{}<{}>: {}", π_v<const char *>,
            "float", π_v<float>) << endl;
    cout << verySimpleFormat("{}<{}>: {}", π_v<const char *>,
            "double", π) << endl;
    cout << verySimpleFormat("{}<{}>: {}", π_v<const char *>,
            "long double", π_v<long double>) << endl;
    return 0;
}
π<int>: 3
π<float>: 3.1415927
π<double>: 3.141592653589793
π<long double>: 3.1415926535897932385

Many common constants were introduced in C++20 using this technique. They are defined in the header <numbers>.

Note: See C++ Metaprogramming: Parameter Packs for a description of verySimpleFormat() and source for ParameterPacks.h.

Template Functions

Template functions are functions that can be parameterized by one or more types. Typically these template parameters are used in the declaration of some or all of the function arguments. This allows C++ to deduce the parameterized types based on the actual arguments used when a template function is invoked. Templates for a min() and max() function are easy to write and illustrate the components of a function template. Historically these functions would have been written as C preprocessor macros, but with templates we can define them in a type-safe way in the C++ language itself.

namespace Utilities {
    template <typename T>
    inline constexpr T min(T a, T b) {
        return a < b ? a : b;
    }

    template <typename T>
    inline constexpr T max(T a, T b) noexcept {
        return a > b ? a : b;
    }
}

Function templates begin with the template keyword followed by a list of parameters in angle brackets. Type parameters are identified by either the keyword class or typename (they are equivalent) followed by the symbol to use for that parameter. In the case of our examples, there is a single type parameter called T. What follows is the C++ definition of a function. The function uses the standard C++ syntax but in each instance where we would use a typename we can instead use the symbol T as needed. When the compiler expands this template it will replace T with the actual type deduced by the use of the template. In our examples we will use the parameter type T to declare the return value of the function and the type of the two function arguments. This is one of the most common usage patterns, but the template parameter T can also be used in the body of the function.

Note: As of C++14 the auto keyword can be used for the return type of the functions which causes the compiler to deduce the return type from the value supplied in the return statement.

Note: We are defining the templates for min() and max() inside the namespace Utilities because the standard C++ library already defines min() and max() and ours would cause a conflict if not wrapped in a namespace. The namespace is not needed for templates. Templates can be declared in the global namespace as well as your own namespaces. Typically template functions would appear in a header file inside the namespace for that header file.

Because we defined only one template type parameter and both arguments to the function are of that type T, when invoked the min() and max() functions can only be expanded to versions where both arguments agree on type. We can not call these functions with arguments of differing types. If we do, we will get a compiler error. Also the type T must be one where the less than operator or greater than operator must be valid. Again if we use a type for T that doesn't have these relational operators, we will get a compile error.

main.cpp
#include <iostream>
#include "Utilities.h"

using namespace std;

int main(int argc, const char * argv[]) {
    std::cout << "C++ Dialect: " << __cplusplus << std::endl;
    cout << "min(3,5) = " << Utilities::min(3,5) << endl;
    cout << "min(3,5) = " << Utilities::max(3,5) << endl;
    cout << "min(3L,5L) = " << Utilities::min(3L,5L) << endl;
    cout << "max(3L,5L) = " << Utilities::max(3L,5L) << endl;
    cout << "min(3.14,5.43) = " << Utilities::min(3.14,5.43) << endl;
    cout << "max(3.14,5.43) = " << Utilities::max(3.14,5.43) << endl;
    cout << "min<int>(3,5.43) = " << Utilities::min<int>(3,5.43) << endl;
    cout << "max<double>(3,5.43) = " << Utilities::max<double>(3,5.43) << endl;
    return 0;
}
C++ Dialect: 202002
min(3,5) = 3
min(3,5) = 5
min(3L,5L) = 3
max(3L,5L) = 5
min(3.14,5.43) = 3.14
max(3.14,5.43) = 5.43
min<int>(3,5.43) = 3
max<double>(3,5.43) = 5.43

In main.cpp the first six calls have arguments that are of the same type so the template functions min() and max()can be expanded by deducing the type of T based on the arguments without any problem. The last two calls have arguments that differ by type. In these cases T cannot be deduced so we get a compiler error such as "No matching function for call to 'min'". To allow the code to compile, we must specify the type of the template function parameter T by adding the type that we want to use in angle brackets after the function name and before the argument list.

Even though there are eight calls to the template functions min() and max()in the example above, only six copies are created and compiled as overloaded min() and max() functions. The family of overloaded functions that get expanded are:

  • min(int a, int b) {...}
  • max(int a, int b) {...}
  • min(long a, long b) {...}
  • max(long a, long b) {...}
  • min(double a, double b) {...}
  • max(double a, double b) {...}

The last two calls to min<int>() and max<double>() reuse the already expanded min(int a, int b) and max(double a, double b) functions respectively that were previously expanded and coerce the arguments to match the argument type.

Utilities.h
//
// Created by Richard Lesh on 10/31/21.
//

#ifndef UTILITIES_H
#define UTILITIES_H

#include <cmath>

namespace Utilities {
    template <typename T>
    inline constexpr T min(T a, T b) {
        return a < b ? a : b;
    }

    template <typename T>
    inline constexpr T max(T a, T b) noexcept {
        return a > b ? a : b;
    }

    template <typename T>
    inline constexpr T abs(T a) noexcept {
        return a < 0 ? -a : a;
    }

    template <typename T>
    inline constexpr int sign(T a) noexcept {
        return a < 0 ? -1 : a > 0 ? 1 : 0;
    }

    template <typename T>
    T gcd(T a, T b) noexcept {
        static_assert(std::is_integral<T>::value);
        if (a < 0) a = -a;
        if (b < 0) b = -b;
        if (a < b) return gcd(b, a);
        if (b == T(0)) return a;
        return gcd(b, a % b);
    }

    template <typename T>
    T fastPow(T b, int n) noexcept {
        T result = T(1);

        while (n > 0) {
            int isOdd = n & 1;
            if (isOdd) {
                result = result * b;
            }
            b *= b;
            n = n >> 1;
        }
        return result;
    }
#endif //UTILITIES_H

Note: Since min(), max(), abs() and sign() only consist of constant expressions, these functions can be labeled with inline constexpr allowing them to be used as compile-time constant expressions or as inline functions if not used in a constexpr context. The functions gcd() and fastPow() use variables so they cannot be labeled constexpr and are probably too large to be labeled inline. Normally non-inline functions placed in a header file would cause linking conflicts when we use multiple compilation units (.cpp files). For template expanded functions the compiler and linker work together to make sure that only one copy of the expanded template function exist for each template type parameter (or combinations if multiple template type parameters are used). So even though you might include Utilities.h in multiple compilation units and use say sign<double>() and sign<int>() in multiple units, you will end up with only one copy of sign<double>() and sign<int>() each.

Template Data Types

As with functions we can write generic struct/class definitions that have a type parameter(s). We start the definition with the template keyword followed by the type parameters in angle brackets. Then we can use the type parameter anywhere we like in the struct/class definition. Typically this type parameter is used to define one or more of the data members in the struct/class. For example we can define a simple generic POD type using the struct keyword.

main.cpp
#include <iostream>

using namespace std;

template <typename T>
struct PointPOD {
    T x, y;
};

int main(int argc, const char * argv[]) {
    PointPOD<short> p0 {3, 5};
    PointPOD<float> p1 {3.14, 5.43};
    PointPOD<double> p2 {3.14, 5.43};
    PointPOD<long double> p3 {3.14, 5.43};
    cout << "PointPOD<short> sizeof = " << sizeof(p0) << endl;
    cout << "PointPOD<float> sizeof = " << sizeof(p1) << endl;
    cout << "PointPOD<double> sizeof = " << sizeof(p2) << endl;
    cout << "PointPOD<long double> sizeof = " << sizeof(p3) << endl;
    return 0;
}
PointPOD<short> sizeof = 4
PointPOD<float> sizeof = 8
PointPOD<double> sizeof = 16
PointPOD<long double> sizeof = 32

The compiler can't deduce the template type for T because we don't have a constructor and thus no arguments to a constructor to use to deduce the type. We have to supply the actual type for the template type parameter inside angle brackets after the struct name when declaring variables of the PointPOD type.

Notice that in our example four different PointPOD types are instantiated each with a differing number of bytes used for storage. Also note that we use the curly brace initializer list to initialize the objects. For POD types the values in the initializer list are applied to the data members in the order that they are declared in the struct definition.

Of course we can also create more sophisticated generic types, typically using the class keyword. We can have not only data members defined using the template parameter T, but also constructors, methods and variables.

main.cpp
#include <iostream>
#include <cmath>

using namespace std;

template <typename T>
class Point {
private:
    T x, y;
public:
    Point(T X, T Y) : x(X), y(Y) {}
    T radius() { return sqrt(x * x + y * y); }
    T angle() { return atan2(y, x); }
};

int main(int argc, const char * argv[]) {
    UnitTest::runtests();
    Point p0 {3, 5};
    Point p1 {3., 5.};
    Point p2 {3., 5.};
    Point p3 {3., 5.};
    cout << "p0.radius = " << p0.radius() << endl;
    cout << "p0.angle = " << p0.angle() << endl;
    cout << std::setprecision(std::numeric_limits<long double>::max_digits10 - 1);
    cout << "p1.radius = " << p1.radius() << endl;
    cout << "p1.angle = " << p1.angle() << endl;
    cout << "p2.radius = " << p2.radius() << endl;
    cout << "p2.angle = " << p2.angle() << endl;
    cout << "p3.radius = " << p3.radius() << endl;
    cout << "p3.angle = " << p3.angle() << endl;
    return 0;
}
p0.radius = 5
p0.angle = 1
p1.radius = 5.830951690673828125
p1.angle = 1.0303767919540405273
p2.radius = 5.8309518948453007425
p2.angle = 1.0303768265243125057
p3.radius = 5.830951894845300471
p3.angle = 1.0303768265243124638

Because we have a user-defined constructor for the Point class, the initializer list syntax calls the constructor by passing the list values as the arguments to the constructor. This allows the template mechanism to deduce the value for the type parameter T based on the types of the constructor actual arguments. This is why we don't have to supply the template type in angle brackets after the class name Point when declaring variables of type Point.

Note: Even though we fix the output precision to the maximum needed to display long doubles, note that the accuracy of the radius() and angle() methods increase from p1 to p2 to p3 as we increase the accuracy of the template parameter T.

The next example illustrates a template class with two type parameters. This Pair class is useful for when we need to return two values of differing types from a function. Typically we will use the letters U, V, and W to refer to the second, third and fourth template arguments if needed.

main.cpp
#include <iostream>
#include <string>

using namespace std;

template <typename T, typename U>
class Pair {
private:
    T _first;
    U _second;
public:
    Pair(T t, U u) : _first(t), _second(u) {}
    T first() const { return _first; }
    U second() const { return _second; }
};

int main(int argc, const char * argv[]) {
    // instantiates class Pair<int, double>
    Pair pair1 { 326, 3.1415 };
    // instantiates class Pair<double, Point<double, double>>
    Pair pair2 { 3.1415, Point(3.,4.) };
    // instantiates class Pair<short, string>
    Pair<short, string> pair3 { 326, "Hello" };

    cout << "pair1: " << pair1.first() << ", " << pair1.second() << endl;
    cout << "pair2: " << pair2.first() << ", ("
        << pair2.second().radius() << ", " << pair2.second().angle() << ")" << endl;
    cout << "pair3: " << pair3.first() << ", " << pair3.second() << endl;
    return 0;
}
pair1: 326, 3.1415
pair2: 3.1415, (5, 0.927295)
pair3: 326, Hello

We can also write template functions with two or more type parameters. This type of function is useful for when we need to pass two values of differing types to the function. For example we can write a make_dynamic_pair() function that returns a dynamically allocated pair object.

template <typename T, typename U>
auto make_dynamic_pair(T t, U u) {
    return unique_ptr<Pair<T, U>>(new Pair(t, u));
}

int main(int argc, const char * argv[]) {
    UnitTest::runtests();
    auto dynamic_pair = make_dynamic_pair(326, 3.14);
    cout << "dynamic_pair: " << dynamic_pair->first()
        << ", " << dynamic_pair->second() << endl;
    return 0;
}