Templates

Why templates?

Everything we have learnt about C++ relies on the fact the type of any object must be known at compile time. This can be quite constricting on the kinds of programs we are able to right. Just think back to Assignment 1 where you were sectioned with creating a class called Triple with a single underlying type (either int, float or double). Imagine you create TripleInt but now had to create a class with the same interface but for the other underlying types (float and double in this case) that you did not implement (well some of you may have). This can dramatically increase code duplication. One might think to solve this through class inheritance. Somehow define a common abstract interface that derived classes could inherit from and then override the methods in order to implement the interface for each underlying type. However, this requires more work still as anyone that wants to implement Triple would have to inherit from it and implement the same interface and logic for there own underlying type. Another caveat is this requires every Triple object to have a vtable to there underlying methods resulting in dynamic dispatch of method calling which, is something that has a lot of runtime overhead. The real solution is to have a single interface that can working for any desired type. This is where templates come in.

What are templates?

Templates can be thought of as blueprints for function, variable or class. They allow for defining interfaces in terms of template parameters which can be types or compile-time variables. Templates can then be instantiated for a given set of valid template parameters which results in the function, variable or class being fully defined meaning they can now be instantiated themselves. Templates are created by using a template<> declaration above the definition of a function, variable or class with the template parameters being specified within the angle brackets (<>).

Templates

Template Parameters

There are three categories of template parameters; type, non-type and template-template. We wont cover template-template parameters as they are used for advanced specialisation. Type template parameters are template parameters that accept a type as an argument. These are used to create generic functions, variables and classes that can be defined in terms of many different types. These are the most common use of templates. Type arguments are declared as template<typename T> with T being the name of the type argument and will be an alias to the type we later instantiate the template with. Non-type arguments are used to pass values at compile time to a function, variable or class. The type of a non-type template argument must be explicitly declared eg. template<int N>, Non-type template arguments are constant and thus cannot be mutated. Here is an example of a template declaration that takes type and non-type template arguments.

template<typename T, std::size_t N>
/// ... template entity details

This is the template signature of std::array.

Template Arguments

Template Functions

To begin, we will look at defining template functions. Template functions are defined identically to regular functions except that before the functions signature we use a template declaration. As we can see below, regardless of the type of the array or the its size we can call print<>(). Also, thanks to C++17 we don't have to call it with specific template arguments as the can be deduced from the array itself.

#include <array>
#include <iostream>
#include <string>

template<typename T, std::size_t N>
auto print(const std::array<T, N>& arr) -> void
{
    std::cout << "[ ";

    for (auto n { N }; const auto& e : arr)
        if (--n > 0)
            std::cout << e << ", ";
        else
            std::cout << e;

    std::cout << " ]" << std::endl;
}

auto main() -> int
{
    auto a1 = std::to_array<int>({ 1, 2, 3, 4, 5 });
    auto a2 = std::to_array<double>({ 1.576, 0.0002, 3756348.34646, 5e-14, 465.7657, 358.0, 237437.456756 });
    auto a3 = std::to_array<std::string>({ "John", "Anna", "Grace", "Bob" });

    print<int, 5>(a1);  ///< Explicit template argument instantiation
    print(a2);          ///< template argument deduction (same below)
    print(a3);

    return 0;
}

Example

Function template

Template Classes

Template classes allow us to define generic types. Almost all of C++ standard library types are template classes, even std::cout which is a static instance of the class std::basic_ostream<char>. Template classes allow us to create a blueprint of a class and then instantiate the class definition which a particular type. We can see this with out Point class from the previous section section. Now we define Point in terms of a template type T which we will specify later. This allows us to create Point objects of float and double as well while still only defining a single interface.

Note: For copy and move constructors, the template type parameters can be elided as copy and move semantics must preserve the type. Converting constructors (a type of explicit constructor) are used to convert between template types.

#include <iostream>
#include <utility>

template<typename T>
class Point
{
public:
    /// Default Constructor (Compiler Created)
    constexpr Point() = default;

    explicit constexpr
    Point(T x, T y) noexcept
        : x{ x }, y{ y }
    { }

    constexpr Point(const Point& p) noexcept
    : x{ p.x }, y{ p.y }
    { }

    constexpr Point(Point&& p) noexcept
        : x{ std::move(p.x) }
        , y{ std::move(p.y) }
    { p = Point(); }

    constexpr auto
    operator= (const Point& p) noexcept -> Point&
    {
        if (p != *this)
        {
            x = p.x;
            y = p.y;
        }

        return *this;
    }

    constexpr auto
    operator= (Point&& p) noexcept -> Point&
    {
        if (p != *this)
        {
            x = std::move(p.x);
            y = std::move(p.y);
        }

        return *this;
    }

    ~Point() noexcept = default;

    constexpr auto
    operator+ (const Point& p) noexcept -> Point
    { return Point{ x + p.x, y + p.y }; }

    constexpr auto
    operator- (const Point& p) noexcept -> Point
    { return Point{ x - p.x, y - p.y }; }

    constexpr auto
    operator== (const Point& p)
        noexcept -> bool
    { return (x == p.x) && (y == p.y); }

    constexpr auto
    operator!= (const Point& p)
    noexcept -> bool
    { return !(*this == p); }

    friend auto
    operator<< (std::ostream& os, const Point& p)
        noexcept -> std::ostream&
    { 
        os << "( "
        << p.x
        << ", "
        << p.y
        << " )";

        return os;
    }

private:
    T x;
    T y;
};


auto main() -> int
{
    auto p1 = Point<int>{ 2, 5 };
    auto p2 = Point{ 6, 7 };
    auto p3 = p1 + p2;
    auto p4 = Point<double>{ 5.6 , -0.007 };
    auto p5 = Point{ 4.576 , 24.012 };
    auto p6 = p4 - p5;

    std::cout << p1 << std::endl;
    std::cout << p2 << std::endl;
    std::cout << p3 << std::endl;
    std::cout << p4 << std::endl;
    std::cout << p5 << std::endl;
    std::cout << p6 << std::endl;

    return 0;
}

Note: We could define Point::x and Point::y to be different types but this is unnecessary for now.

Example

Class Template

Class Template Methods

Just like how free functions can be templated, we can also specify methods of classes to be templated. This allows use to customize classes methods not just in terms of its template types and parameters but also in terms of templates of objects that we may need to pass to the method of a class. This useful for creating overloaded methods that are not just customized by type but also the template signature pattern they have.

You may have noticed that I did not add or subtract two Point of different type in the previous example. This is because + and - are only defined for Point of the same template. We can fix this with a templated method. There are a few other details we have to add in order for this to work. First we add the template specification for the other Point<U> argument (we can do the same for == and !=). We then us std::common_type<T...> to help deduce the arithmetic (an other) promotions between types. This is for promotions of say int to double. Finally, because the class Point<U> is a different instantiation of Point to this (ie. Point<T>), we have to specify all Point<U> of any U type as a friend class to Point<T>.

/// ... Point details

template<typename U>
constexpr auto
operator+ (const Point<U>& p) 
    noexcept -> Point<typename std::common_type<T, U>::type>
{ return Point<typename std::common_type<T, U>::type>{ x + p.x, y + p.y }; }

template<typename U>
constexpr auto
operator- (const Point<U>& p) 
    noexcept -> Point<typename std::common_type<T, U>::type>
{ return Point<typename std::common_type<T, U>::type>{ x - p.x, y - p.y }; }

template<typename U>
constexpr auto
operator== (const Point<U>& p)
    noexcept -> bool
{ return (x == p.x) && (y == p.y); }

template<typename U>
constexpr auto
operator!= (const Point<U>& p)
noexcept -> bool
{ return !(*this == p); }

/// ... Point details

private:

    T x;
    T y;

    template<typename U>
    friend class Point;

Note: The template argument names must be different then the outer template classes template argument names as this name (symbol) is still very much in scope.

Example

Section 2.6: Template Variables

Is is also possible to define variables as templates. This allows for variables to take on different forms depending on the underlying type. Template variables are typically static or constexpr free variables or static members of a class. A common use of template variables is mathematical constants that have different underlying types. We can see this in use with e<T> and std::numeric_limits<T> members.

#include <iomanip>
#include <iostream>
#include <numeric>

template<typename T>
constexpr T e = T(2.7182818284590452353602874713527);

auto main() -> int
{
    std::cout << std::setprecision(std::numeric_limits<long double>::max_digits10);
    std::cout << "True e            = 2.7182818284590452353602874713527" << std::endl;
    std::cout << "e<long double>    = " << e<long double> << std::endl;
    std::cout << "e<double>         = " << e<double> << std::endl;
    std::cout << "e<float>          = " << e<float> << std::endl;
    std::cout << "e<int>            = " << e<int> << std::endl;
    std::cout << "e<char>           = " << e<char> << std::endl;
    
    return 0;
}

Note: Consequently, this is a good showcase of how quickly float and double loose precision.

Example

Template Metaprogramming Basics

We've seen a bit of metaprogramming in C++ already. Metaprogramming is the ability to influence the structure of your code at compile time using the language itself. Templates are a simple form of metaprogramming and allow us to change the behaviour of our code based on the types and values we instantiate our templates with. Using templates, the C++ standard library has many metaprogramming objects that allow us to customize our code even more. Most of the standard libraries metaprogramming objects are in the form of type analysis from the <type_traits> header. These allow us to inspect the properties of any type (template or known) and customize based on the results. There is much more to metaprogramming that is beyond the scope of this course.