22
Advanced Templates

MORE ABOUT TEMPLATE PARAMETERS

There are actually three kinds of template parameters: type, non-type, and template template (no, you’re not seeing double: that really is the name). So far, you’ve seen examples of type and non-type parameters (in Chapter 12), but not template template parameters. There are also some tricky aspects to both type and non-type parameters that are not covered in Chapter 12. This section goes deeper into all three types of template parameters.

More about Template Type Parameters

Template type parameters are the main purpose of templates. You can declare as many type parameters as you want. For example, you could add to the grid template from Chapter 12 a second type parameter specifying another templatized class container on which to build the grid. The Standard Library defines several templatized container classes, including vector and deque. The original grid class uses a vector of vectors to store the elements of a grid. A user of the Grid class might want to use a vector of deques instead. With another template type parameter, you can allow the user to specify whether they want the underlying container to be a vector or a deque. Here is the class definition with the additional template parameter:

template <typename T, typename Container>
class Grid
{
    public:
        explicit Grid(size_t width = kDefaultWidth,
            size_t height = kDefaultHeight);
        virtual ~Grid() = default;

        // Explicitly default a copy constructor and assignment operator.
        Grid(const Grid& src) = default;
        Grid<T, Container>& operator=(const Grid& rhs) = default;

        // Explicitly default a move constructor and assignment operator.
        Grid(Grid&& src) = default;
        Grid<T, Container>& operator=(Grid&& rhs) = default;

        typename Container::value_type& at(size_t x, size_t y);
        const typename Container::value_type& at(size_t x, size_t y) const;

        size_t getHeight() const { return mHeight; }
        size_t getWidth() const { return mWidth; }

        static const size_t kDefaultWidth = 10;
        static const size_t kDefaultHeight = 10;

    private:
        void verifyCoordinate(size_t x, size_t y) const;

        std::vector<Container> mCells;
        size_t mWidth = 0, mHeight = 0;
};

This template now has two parameters: T and Container. Thus, wherever you previously referred to Grid<T>, you must now refer to Grid<T, Container> to specify both template parameters. Another change is that mCells is now a vector of Containers instead of a vector of vectors.

Here is the constructor definition:

template <typename T, typename Container>
Grid<T, Container>::Grid(size_t width, size_t height)
    : mWidth(width), mHeight(height)
{
    mCells.resize(mWidth);
    for (auto& column : mCells) {
        column.resize(mHeight);
    }
}

This constructor assumes that the Container type has a resize() method. If you try to instantiate this template by specifying a type that has no resize() method, the compiler will generate an error.

The return type of the at() methods is the type of the elements that is stored inside the given container type. You can access this type with typename Container::value_type.

Here are the implementations of the remaining methods:


template <typename T, typename Container>
void Grid<T, Container>::verifyCoordinate(size_t x, size_t y) const
{
    if (x >= mWidth || y >= mHeight) {
        throw std::out_of_range("");
    }
}

template <typename T, typename Container>
const typename Container::value_type&
    Grid<T, Container>::at(size_t x, size_t y) const
{
    verifyCoordinate(x, y);
    return mCells[x][y];
}

template <typename T, typename Container>
typename Container::value_type&
    Grid<T, Container>::at(size_t x, size_t y)
{
    return const_cast<typename Container::value_type&>(
        std::as_const(*this).at(x, y));
}

Now you can instantiate and use Grid objects like this:

Grid<int, vector<optional<int>>> myIntVectorGrid;
Grid<int, deque<optional<int>>> myIntDequeGrid;

myIntVectorGrid.at(3, 4) = 5;
cout << myIntVectorGrid.at(3, 4).value_or(0) << endl;

myIntDequeGrid.at(1, 2) = 3;
cout << myIntDequeGrid.at(1, 2).value_or(0) << endl;

Grid<int, vector<optional<int>>> grid2(myIntVectorGrid);
grid2 = myIntVectorGrid;

The use of the word Container for the parameter name doesn’t mean that the type really must be a container. You could try to instantiate the Grid class with an int instead:

Grid<int, int> test; // WILL NOT COMPILE

This line does not compile, but it also might not give you the error you expect. It does not complain that the second type argument is an int instead of a container. It tells you something more cryptic. For example, Microsoft Visual C++ tells you that “‘Container’: must be a class or namespace when followed by ‘::’.” That’s because the compiler attempts to generate a Grid class with int as the Container. Everything works fine until it tries to process this line in the class template definition:

typename Container::value_type& at(size_t x, size_t y);

At that point, the compiler realizes that Container is an int, which does not have an embedded value_type type alias.

Just as with function parameters, you can give template parameters default values. For example, you might want to say that the default container for your Grid is a vector. The class template definition would look like this:

template <typename T, typename Container = std::vector<std::optional<T>>>
class Grid
{
    // Everything else is the same as before.
};

You can use the type T from the first template parameter as the argument to the optional template in the default value for the second template parameter. The C++ syntax requires that you do not repeat the default value in the template header line for method definitions. With this default argument, clients can now instantiate a grid with the option of specifying an underlying container:

Grid<int, deque<optional<int>>> myDequeGrid;
Grid<int, vector<optional<int>>> myVectorGrid;
Grid<int> myVectorGrid2(myVectorGrid);

This approach is used by the Standard Library. The stack, queue, and priority_queue class templates all take a template type parameter, with a default value, specifying the underlying container.

Introducing Template Template Parameters

There is one problem with the Container parameter in the previous section. When you instantiate the class template, you write something like this:

Grid<int, vector<optional<int>>> myIntGrid;

Note the repetition of the int type. You must specify that it’s the element type both of the Grid and of the optional inside the vector. What if you wrote this instead:

Grid<int, vector<optional<SpreadsheetCell>>> myIntGrid;

That wouldn’t work very well. It would be nice to be able to write the following, so that you couldn’t make that mistake:

Grid<int, vector> myIntGrid;

The Grid class should be able to figure out that it wants a vector of optionals of ints. The compiler won’t allow you to pass that argument to a normal type parameter, though, because vector by itself is not a type but a template.

If you want to take a template as a template parameter, you must use a special kind of parameter called a template template parameter. Specifying a template template parameter is sort of like specifying a function pointer parameter in a normal function. Function pointer types include the return type and parameter types of a function. Similarly, when you specify a template template parameter, the full specification of the template template parameter includes the parameters to that template.

For example, containers such as vector and deque have a template parameter list that looks something like the following. The E parameter is the element type. The Allocator parameter is covered in Chapter 17.

template <typename E, typename Allocator = std::allocator<E>>
class vector
{
    // Vector definition
};

To pass such a container as a template template parameter, all you have to do is copy and paste the declaration of the class template (in this example, template <typename E, typename Allocator = std::allocator<E>> class vector), replace the class name (vector) with a parameter name (Container), and use that as the template template parameter of another template declaration—Grid in this example—instead of a simple type name. Given the preceding template specification, here is the class template definition for the Grid class that takes a container template as its second template parameter:

template <typename T,
 template <typename E, typename Allocator = std::allocator<E>> class Container
    = std::vector>
class Grid
{
    public:
        // Omitted code that is the same as before
        std::optional<T>& at(size_t x, size_t y);
        const std::optional<T>& at(size_t x, size_t y) const;
        // Omitted code that is the same as before
    private:
        void verifyCoordinate(size_t x, size_t y) const;

        std::vector<Container<std::optional<T>>> mCells;
        size_t mWidth = 0, mHeight = 0;
};

What is going on here? The first template parameter is the same as before: the element type T. The second template parameter is now a template itself for a container such as vector or deque. As you saw earlier, this “template type” must take two parameters: an element type E and an allocator type. Note the repetition of the word class after the nested template parameter list. The name of this parameter in the Grid template is Container (as before). The default value is now vector, instead of vector<T>, because the Container parameter is now a template instead of an actual type.

The syntax rule for a template template parameter, more generically, is this:

template <..., template <TemplateTypeParams> class ParameterName, ...>

 

Instead of using Container by itself in the code, you must specify Container<std::optional<T>> as the container type. For example, the declaration of mCells is now as follows:

std::vector<Container<std::optional<T>>> mCells;

The method definitions don’t need to change, except that you must change the template lines, for example:

template <typename T,
 template <typename E, typename Allocator = std::allocator<E>> class Container>
void Grid<T, Container>::verifyCoordinate(size_t x, size_t y) const
{
    if (x >= mWidth || y >= mHeight) {
        throw std::out_of_range("");
    }
}

This Grid template can be used as follows:

Grid<int, vector> myGrid;
myGrid.at(1, 2) = 3;
cout << myGrid.at(1, 2).value_or(0) << endl;
Grid<int, vector> myGrid2(myGrid);

This C++ syntax is a bit convoluted because it is trying to allow for maximum flexibility. Try not to get bogged down in the syntax here, and keep the main concept in mind: you can pass templates as parameters to other templates.

More about Non-type Template Parameters

You might want to allow the user to specify a default element used to initialize each cell in the grid. Here is a perfectly reasonable approach to implement this goal. It uses T() as the default value for the second template parameter.

template <typename T, const T DEFAULT = T()>
class Grid
{
    // Identical as before.
};

This definition is legal. You can use the type T from the first parameter as the type for the second parameter, and non-type parameters can be const just like function parameters. You can use this initial value for T to initialize each cell in the grid:

template <typename T, const T DEFAULT>
Grid<T, DEFAULT>::Grid(size_t width, size_t height)
        : mWidth(width), mHeight(height)
{
    mCells.resize(mWidth);
    for (auto& column : mCells) {
        column.resize(mHeight);
        for (auto& element : column) {
            element = DEFAULT;
        }
    }
}

The other method definitions stay the same, except that you must add the second template parameter to the template lines, and all the instances of Grid<T> become Grid<T, DEFAULT>. After making those changes, you can instantiate grids with an initial value for all the elements:

Grid<int> myIntGrid;       // Initial value is 0
Grid<int, 10> myIntGrid2;  // Initial value is 10

The initial value can be any integer you want. However, suppose that you try to create a SpreadsheetCell Grid:

SpreadsheetCell defaultCell;
Grid<SpreadsheetCell, defaultCell> mySpreadsheet; // WILL NOT COMPILE

That line leads to a compiler error because you cannot pass objects as arguments to non-type parameters.

This example illustrates one of the vagaries of class templates: they can work correctly on one type but fail to compile for another type.

A more comprehensive way of allowing the user to specify an initial element value for a grid uses a reference to a T as the non-type template parameter. Here is the new class definition:

template <typename T, const T& DEFAULT>
class Grid
{
    // Everything else is the same as the previous example.
};

Now you can instantiate this class template for any type. The C++17 standard says that the reference you pass as the second template argument must be a converted constant expression of the type of the template parameter. It is not allowed to refer to a subobject, a temporary object, a string literal, the result of a typeid expression, or the predefined __func__ variable. The following example declares int and SpreadsheetCell grids using initial values:

int main()
{
    int defaultInt = 11;
    Grid<int, defaultInt> myIntGrid;

    SpreadsheetCell defaultCell(1.2);
    Grid<SpreadsheetCell, defaultCell> mySpreadsheet;
    return 0;
}

However, those are the rules of C++17, and unfortunately, most compilers do not implement those rules yet. Before C++17, an argument passed to a reference non-type template parameter could not be a temporary, and could not be a named lvalue without linkage (external or internal). So, here is the same example but using the pre-C++17 rules. The initial values are defined with internal linkage:

namespace {
    int defaultInt = 11;
    SpreadsheetCell defaultCell(1.2);
}

int main()
{
    Grid<int, defaultInt> myIntGrid;
    Grid<SpreadsheetCell, defaultCell> mySpreadsheet;
    return 0;
}

CLASS TEMPLATE PARTIAL SPECIALIZATION

The const char* class specialization of the Grid class template shown in Chapter 12 is called a full class template specialization because it specializes the Grid template for every template parameter. There are no template parameters left in the specialization. That’s not the only way you can specialize a class; you can also write a partial class specialization, in which you specialize some template parameters but not others. For example, recall the basic version of the Grid template with width and height non-type parameters:

template <typename T, size_t WIDTH, size_t HEIGHT>
class Grid
{
    public:
        Grid() = default;
        virtual ~Grid() = default;

        // Explicitly default a copy constructor and assignment operator.
        Grid(const Grid& src) = default;
        Grid& operator=(const Grid& rhs) = default;

        std::optional<T>& at(size_t x, size_t y);
        const std::optional<T>& at(size_t x, size_t y) const; 

        size_t getHeight() const { return HEIGHT; }
        size_t getWidth() const { return WIDTH; }
    private:
        void verifyCoordinate(size_t x, size_t y) const;

        std::optional<T> mCells[WIDTH][HEIGHT];
};

You could specialize this class template for const char* C-style strings like this:

#include "Grid.h" // The file containing the Grid template definition

template <size_t WIDTH, size_t HEIGHT>
class Grid<const char*, WIDTH, HEIGHT>
{
    public:
        Grid() = default;
        virtual ~Grid() = default;

// Explicitly default a copy constructor and assignment operator.
        Grid(const Grid& src) = default;
        Grid& operator=(const Grid& rhs) = default;

        std::optional<std::string>& at(size_t x, size_t y);
        const std::optional<std::string>& at(size_t x, size_t y) const;

        size_t getHeight() const { return HEIGHT; }
        size_t getWidth() const { return WIDTH; }
    private:
        void verifyCoordinate(size_t x, size_t y) const;

        std::optional<std::string> mCells[WIDTH][HEIGHT];
};

In this case, you are not specializing all the template parameters. Therefore, your template line looks like this:

template <size_t WIDTH, size_t HEIGHT>
class Grid<const char*, WIDTH, HEIGHT>

Note that the template has only two parameters: WIDTH and HEIGHT. However, you’re writing a Grid class for three arguments: T, WIDTH, and HEIGHT. Thus, your template parameter list contains two parameters, and the explicit Grid<const char*, WIDTH, HEIGHT> contains three arguments. When you instantiate the template, you must still specify three parameters. You can’t instantiate the template with only height and width.

Grid<int, 2, 2> myIntGrid;            // Uses the original Grid
Grid<const char*, 2, 2> myStringGrid; // Uses the partial specialization
Grid<2, 3> test;                      // DOES NOT COMPILE! No type specified.

Yes, the syntax is confusing. And it gets worse. In partial specializations, unlike in full specializations, you include the template line in front of every method definition, as in the following example:

template <size_t WIDTH, size_t HEIGHT>
const std::optional<std::string>&
    Grid<const char*, WIDTH, HEIGHT>::at(size_t x, size_t y) const
{
    verifyCoordinate(x, y);
    return mCells[x][y];
}

You need this template line with two parameters to show that this method is parameterized on those two parameters. Note that wherever you refer to the full class name, you must use Grid<const char*, WIDTH, HEIGHT>.

The previous example does not show the true power of partial specialization. You can write specialized implementations for a subset of possible types without specializing individual types. For example, you can write a specialization of the Grid class template for all pointer types. The copy constructor and assignment operator of this specialization could perform deep copies of objects to which pointers point, instead of shallow copies.

Here is the class definition, assuming that you’re specializing the initial version of Grid with only one parameter. In this implementation, Grid becomes the owner of supplied data, so it automatically frees the memory when necessary.

#include "Grid.h"
#include <memory>

template <typename T>
class Grid<T*>
{
    public:
        explicit Grid(size_t width = kDefaultWidth,
            size_t height = kDefaultHeight);
        virtual ~Grid() = default;

        // Copy constructor and copy assignment operator.
        Grid(const Grid& src);
        Grid<T*>& operator=(const Grid& rhs);

        // Explicitly default a move constructor and assignment operator.
        Grid(Grid&& src) = default;
        Grid<T*>& operator=(Grid&& rhs) = default;

        void swap(Grid& other) noexcept;

        std::unique_ptr<T>& at(size_t x, size_t y);
        const std::unique_ptr<T>& at(size_t x, size_t y) const;

        size_t getHeight() const { return mHeight; }
        size_t getWidth() const { return mWidth; }

        static const size_t kDefaultWidth = 10;
        static const size_t kDefaultHeight = 10;

    private:
        void verifyCoordinate(size_t x, size_t y) const;

        std::vector<std::vector<std::unique_ptr<T>>> mCells;
        size_t mWidth = 0, mHeight = 0;
};

As usual, these two lines are the crux of the matter:

template <typename T>
class Grid<T*>

The syntax says that this class is a specialization of the Grid template for all pointer types. You are providing the implementation only in cases where T is a pointer type. Note that if you instantiate a grid like Grid<int*> myIntGrid, then T will actually be int, not int*. That’s a bit unintuitive, but unfortunately, that’s the way it works. Here is an example of using this partial specialization:

Grid<int> myIntGrid;     // Uses the non-specialized grid
Grid<int*> psGrid(2, 2); // Uses the partial specialization for pointer types

psGrid.at(0, 0) = make_unique<int>(1);
psGrid.at(0, 1) = make_unique<int>(2);
psGrid.at(1, 0) = make_unique<int>(3);

Grid<int*> psGrid2(psGrid);
Grid<int*> psGrid3;
psGrid3 = psGrid2;

auto& element = psGrid2.at(1, 0);
if (element) {
    cout << *element << endl;
    *element = 6;
}
cout << *psGrid.at(1, 0) << endl;  // psGrid is not modified
cout << *psGrid2.at(1, 0) << endl; // psGrid2 is modified

Here is the output:

3
3
6

The implementations of the methods are rather straightforward, except for the copy constructor, which uses the copy constructor of individual elements to make a deep copy of them:

template <typename T>
Grid<T*>::Grid(const Grid& src)
    : Grid(src.mWidth, src.mHeight)
{
    // The ctor-initializer of this constructor delegates first to the
    // non-copy constructor to allocate the proper amount of memory.

    // The next step is to copy the data.
    for (size_t i = 0; i < mWidth; i++) {
        for (size_t j = 0; j < mHeight; j++) {
            // Make a deep copy of the element by using its copy constructor.
            if (src.mCells[i][j]) {
                mCells[i][j].reset(new T(*(src.mCells[i][j])));
            }
        }
    }
}

EMULATING FUNCTION PARTIAL SPECIALIZATION WITH OVERLOADING

The C++ standard does not permit partial template specialization of functions. Instead, you can overload the function with another template. The difference is subtle. Suppose that you want to write a specialization of the Find() function template, presented in Chapter 12, that dereferences the pointers to use operator== directly on the objects pointed to. Following the syntax for class template partial specialization, you might be tempted to write this:

template <typename T>
size_t Find<T*>(T* const& value, T* const* arr, size_t size)
{
    for (size_t i = 0; i < size; i++) {
        if (*arr[i] == *value) {
            return i; // Found it; return the index
        }
    }
    return NOT_FOUND; // failed to find it; return NOT_FOUND
}

However, that syntax declares a partial specialization of the function template, which the C++ standard does not allow. The correct way to implement the behavior you want is to write a new template for Find(). The difference might seem trivial and academic, but it won’t compile otherwise.

template <typename T>
size_t Find(T* const& value, T* const* arr, size_t size)
{
    for (size_t i = 0; i < size; i++) {
        if (*arr[i] == *value) {
            return i; // Found it; return the index
        }
    }
    return NOT_FOUND; // failed to find it; return NOT_FOUND
}

Note that the first parameter to this version of Find() is T* const&. This is done to make it symmetric with the original Find() function template, which accepts a const T& as a first parameter. However, in this case, using T* instead of T* const& for the first parameter of the partial specialization of Find() works as well.

You can define in one program the original Find() template, the overloaded Find() for partial specialization on pointer types, the complete specialization for const char*s, and the overloaded Find() just for const char*s. The compiler selects the appropriate version to call based on its deduction rules.

The following code calls Find() several times. The comments say which version of Find() is called.

size_t res = NOT_FOUND;

int myInt = 3, intArray[] = { 1, 2, 3, 4 };
size_t sizeArray = std::size(intArray);
res = Find(myInt, intArray, sizeArray);    // calls Find<int> by deduction
res = Find<int>(myInt, intArray, sizeArray); // calls Find<int> explicitly

double myDouble = 5.6, doubleArray[] = { 1.2, 3.4, 5.7, 7.5 };
sizeArray = std::size(doubleArray);
// calls Find<double> by deduction
res = Find(myDouble, doubleArray, sizeArray);
// calls Find<double> explicitly
res = Find<double>(myDouble, doubleArray, sizeArray);

const char* word = "two";
const char* words[] = { "one", "two", "three", "four" };
sizeArray = std::size(words);
// calls template specialization for const char*s
res = Find<const char*>(word, words, sizeArray);  
// calls overloaded Find for const char*s
res = Find(word, words, sizeArray);

int *intPointer = &myInt, *pointerArray[] = { &myInt, &myInt };
sizeArray = std::size(pointerArray);
// calls the overloaded Find for pointers
res = Find(intPointer, pointerArray, sizeArray);

SpreadsheetCell cell1(10), cellArray[] = { SpreadsheetCell(4), SpreadsheetCell(10) };
sizeArray = std::size(cellArray);
// calls Find<SpreadsheetCell> by deduction
res = Find(cell1, cellArray, sizeArray); 
// calls Find<SpreadsheetCell> explicitly
res = Find<SpreadsheetCell>(cell1, cellArray, sizeArray);

SpreadsheetCell *cellPointer = &cell1;
SpreadsheetCell *cellPointerArray[] = { &cell1, &cell1 };
sizeArray = std::size(cellPointerArray);
// Calls the overloaded Find for pointers
res = Find(cellPointer, cellPointerArray, sizeArray);

TEMPLATE RECURSION

Templates in C++ provide capabilities that go far beyond the simple classes and functions you have seen so far in this chapter and Chapter 12. One of these capabilities is template recursion. This section first provides a motivation for template recursion, and then shows how to implement it.

This section uses operator overloading, discussed in Chapter 15. If you skipped that chapter or are unfamiliar with the syntax for overloading operator[], consult Chapter 15 before continuing.

An N-Dimensional Grid: First Attempt

Up to now, the Grid template example supported only two dimensions, which limited its usefulness. What if you wanted to write a 3-D Tic-Tac-Toe game or write a math program with four-dimensional matrices? You could, of course, write a templated or non-templated class for each of those dimensions. However, that would repeat a lot of code. Another approach would be to write only a single-dimensional grid. Then, you could create a Grid of any dimension by instantiating the Grid with another Grid as its element type. This Grid element type could itself be instantiated with a Grid as its element type, and so on. Here is the implementation of the OneDGrid class template. It’s simply a one-dimensional version of the Grid template from earlier examples, with the addition of a resize() method, and the substitution of operator[] for at(). Just as with Standard Library containers such as vector, the operator[] implementation does not perform any bounds checking. Also, for this example, mElements stores instances of T instead of instances of std::optional<T>.

template <typename T>
class OneDGrid
{
    public:
        explicit OneDGrid(size_t size = kDefaultSize);
        virtual ~OneDGrid() = default;

        T& operator[](size_t x);
        const T& operator[](size_t x) const;

        void resize(size_t newSize);
        size_t getSize() const { return mElements.size(); }

        static const size_t kDefaultSize = 10;
    private:
        std::vector<T> mElements;
};

template <typename T>
OneDGrid<T>::OneDGrid(size_t size)
{
    resize(size);
}

template <typename T>
void OneDGrid<T>::resize(size_t newSize)
{
    mElements.resize(newSize);
}

template <typename T>
T& OneDGrid<T>::operator[](size_t x)
{
    return mElements[x];
}

template <typename T>
const T& OneDGrid<T>::operator[](size_t x) const
{
    return mElements[x];
}

With this implementation of OneDGrid, you can create multidimensional grids like this:

OneDGrid<int> singleDGrid;
OneDGrid<OneDGrid<int>> twoDGrid;
OneDGrid<OneDGrid<OneDGrid<int>>> threeDGrid;
singleDGrid[3] = 5;
twoDGrid[3][3] = 5;
threeDGrid[3][3][3] = 5;

This code works fine, but the declarations are messy. As you will see in the next section, you can do better.

A Real N-Dimensional Grid

You can use template recursion to write a “real” N-dimensional grid because dimensionality of grids is essentially recursive. You can see that in this declaration:

OneDGrid<OneDGrid<OneDGrid<int>>> threeDGrid;

You can think of each nested OneDGrid as a recursive step, with the OneDGrid of int as the base case. In other words, a three-dimensional grid is a single-dimensional grid of single-dimensional grids of single-dimensional grids of ints. Instead of requiring the user to do this recursion, you can write a class template that does it for you. You can then create N-dimensional grids like this:

NDGrid<int, 1> singleDGrid;
NDGrid<int, 2> twoDGrid;
NDGrid<int, 3> threeDGrid;

The NDGrid class template takes a type for its element and an integer specifying its “dimensionality.” The key insight here is that the element type of the NDGrid is not the element type specified in the template parameter list, but is in fact another NDGrid of dimensionality one less than the current one. In other words, a three-dimensional grid is a vector of two-dimensional grids; the two-dimensional grids are each vectors of one-dimensional grids.

With recursion, you need a base case. You can write a partial specialization of the NDGrid for dimensionality of 1, in which the element type is not another NDGrid, but is in fact the element type specified by the template parameter.

Here is the general NDGrid template definition, with highlights showing where it differs from the OneDGrid shown in the previous section:

template <typename T, size_t N>
class NDGrid
{
    public:
        explicit NDGrid(size_t size = kDefaultSize);
        virtual ~NDGrid() = default;

        NDGrid<T, N-1>& operator[](size_t x);
        const NDGrid<T, N-1>& operator[](size_t x) const;

        void resize(size_t newSize);
        size_t getSize() const { return mElements.size(); }

        static const size_t kDefaultSize = 10;
    private:
        std::vector<NDGrid<T, N-1>> mElements;
};

Note that mElements is a vector of NDGrid<T, N-1>; this is the recursive step. Also, operator[] returns a reference to the element type, which is again NDGrid<T, N-1>, not T.

The template definition for the base case is a partial specialization for dimension 1:

template <typename T>
class NDGrid<T, 1>
{
    public:
        explicit NDGrid(size_t size = kDefaultSize);
        virtual ~NDGrid() = default;

        T& operator[](size_t x);
        const T& operator[](size_t x) const;

        void resize(size_t newSize);
        size_t getSize() const { return mElements.size(); }

        static const size_t kDefaultSize = 10;
    private:
        std::vector<T> mElements;
};

Here the recursion ends: the element type is T, not another template instantiation.

The trickiest aspect of the implementations, other than the template recursion itself, is appropriately sizing each dimension of the grid. This implementation creates the N-dimensional grid with every dimension of equal size. It’s significantly more difficult to specify a separate size for each dimension. However, even with this simplification, there is still a problem: the user should have the ability to create the array with a specified size, such as 20 or 50. Thus, the constructor takes an integer size parameter. However, when you dynamically resize a vector of sub-grids, you cannot pass this size value on to the sub-grid elements because vectors create objects using their default constructor. Thus, you must explicitly call resize() on each grid element of the vector. That code follows. The base case doesn’t need to resize its elements because the elements are Ts, not grids.

Here are the implementations of the general NDGrid template, with highlights showing the differences from the OneDGrid:

template <typename T, size_t N>
NDGrid<T, N>::NDGrid(size_t size)
{
    resize(size);
}

template <typename T, size_t N>
void NDGrid<T, N>::resize(size_t newSize)
{
    mElements.resize(newSize);
    // Resizing the vector calls the 0-argument constructor for
    // the NDGrid<T, N-1> elements, which constructs
    // them with the default size. Thus, we must explicitly call
    // resize() on each of the elements to recursively resize all
    // nested Grid elements.
    for (auto& element : mElements) {
        element.resize(newSize);
    }
}

template <typename T, size_t N>
NDGrid<T, N-1>& NDGrid<T, N>::operator[](size_t x)
{
    return mElements[x];
}

template <typename T, size_t N>
const NDGrid<T, N-1>& NDGrid<T, N>::operator[](size_t x) const
{
    return mElements[x];
}

Now, here are the implementations of the partial specialization (base case). Note that you must rewrite a lot of the code because you don’t inherit any implementations with specializations. Highlights show the differences from the non-specialized NDGrid.

template <typename T>
NDGrid<T, 1>::NDGrid(size_t size)
{
    resize(size);
}

template <typename T>
void NDGrid<T, 1>::resize(size_t newSize)
{
    mElements.resize(newSize);
}

template <typename T>
T& NDGrid<T, 1>::operator[](size_t x)
{
    return mElements[x];
}

template <typename T>
const T& NDGrid<T, 1>::operator[](size_t x) const
{
    return mElements[x];
}

Now, you can write code like this:

NDGrid<int, 3> my3DGrid;
my3DGrid[2][1][2] = 5;
my3DGrid[1][1][1] = 5;
cout << my3DGrid[2][1][2] << endl;

VARIADIC TEMPLATES

Normal templates can take only a fixed number of template parameters. Variadic templates can take a variable number of template parameters. For example, the following code defines a template that can accept any number of template parameters, using a parameter pack called Types:

template<typename... Types>
class MyVariadicTemplate { };

You can instantiate MyVariadicTemplate with any number of types, as in this example:

MyVariadicTemplate<int> instance1;
MyVariadicTemplate<string, double, list<int>> instance2;

It can even be instantiated with zero template arguments:

MyVariadicTemplate<> instance3;

To avoid instantiating a variadic template with zero template arguments, you can write your template as follows:

template<typename T1, typename... Types>
class MyVariadicTemplate { };

With this definition, trying to instantiate MyVariadicTemplate with zero template arguments results in a compiler error. For example, with Microsoft Visual C++ you get the following error:

error C2976: 'MyVariadicTemplate' : too few template arguments

It is not possible to directly iterate over the different arguments given to a variadic template. The only way you can do this is with the aid of template recursion. The following sections show two examples of how to use variadic templates.

Type-Safe Variable-Length Argument Lists

Variadic templates allow you to create type-safe variable-length argument lists. The following example defines a variadic template called processValues(), allowing it to accept a variable number of arguments with different types in a type-safe manner. The processValues() function processes each value in the variable-length argument list and executes a function called handleValue() for each single argument. This means that you have to write a handleValue() function for each type that you want to handle—int, double, and string in this example:

void handleValue(int value) { cout << "Integer: " << value << endl; }
void handleValue(double value) { cout << "Double: " << value << endl; }
void handleValue(string_view value) { cout << "String: " << value << endl; }

void processValues() { /* Nothing to do in this base case.*/ }

template<typename T1, typename... Tn>
void processValues(T1 arg1, Tn... args)
{
    handleValue(arg1);
    processValues(args...);
}

What this example also demonstrates is the double use of the triple dots ... operator. This operator appears in three places and has two different meanings. First, it is used after typename in the template parameter list and after type Tn in the function parameter list. In both cases it denotes a parameter pack. A parameter pack can accept a variable number of arguments.

The second use of the ... operator is following the parameter name args in the function body. In this case, it means a parameter pack expansion; the operator unpacks/expands the parameter pack into separate arguments. It basically takes what is on the left side of the operator, and repeats it for every template parameter in the pack, separated by commas. Take the following line:

processValues(args...);

This line unpacks/expands the args parameter pack into its separate arguments, separated by commas, and then calls the processValues() function with the list of expanded arguments. The template always requires at least one template parameter, T1. The act of recursively calling processValues() with args... is that on each call there is one template parameter less.

Because the implementation of the processValues() function is recursive, you need to have a way to stop the recursion. This is done by implementing a processValues() function that accepts no arguments.

You can test the processValues() variadic template as follows:

processValues(1, 2, 3.56, "test", 1.1f);

The recursive calls generated by this example are as follows:

processValues(1, 2, 3.56, "test", 1.1f);
  handleValue(1);
  processValues(2, 3.56, "test", 1.1f);
    handleValue(2);
    processValues(3.56, "test", 1.1f);
      handleValue(3.56);
      processValues("test", 1.1f);
        handleValue("test");
        processValues(1.1f);
          handleValue(1.1f);
          processValues();

It is important to remember that this method of variable-length argument lists is fully type-safe. The processValues() function automatically calls the correct handleValue() overload based on the actual type. Automatic casting can happen as usual in C++. For example, the 1.1f in the preceding example is of type float. The processValues() function calls handleValue(double value) because conversion from float to double is without any loss. However, the compiler will issue an error when you call processValues() with an argument of a certain type for which there is no handleValue() defined.

There is a problem, though, with the preceding implementation. Because it’s a recursive implementation, the parameters are copied for each recursive call to processValues(). This can become costly depending on the type of the arguments. You might think that you can avoid this copying by passing references to processValues() instead of using pass-by-value. Unfortunately, that also means that you cannot call processValues() with literals anymore, because a reference to a literal value is not allowed, unless you use const references.

To use non-const references and still allow literal values, you can use forwarding references. The following implementation uses forwarding references, T&&, and uses std::forward() for perfect forwarding of all parameters. Perfect forwarding means that if an rvalue is passed to processValues(), it is forwarded as an rvalue reference. If an lvalue or lvalue reference is passed, it is forwarded as an lvalue reference.

void processValues() { /* Nothing to do in this base case.*/ }

template<typename T1, typename... Tn>
void processValues(T1&& arg1, Tn&&... args)
{
    handleValue(std::forward<T1>(arg1));
    processValues(std::forward<Tn>(args)...);
}

There is one line that needs further explanation:

processValues(std::forward<Tn>(args)...);

The ... operator is used to unpack the parameter pack. It uses std::forward() on each individual argument in the pack and separates them with commas. For example, suppose args is a parameter pack with three arguments, a1, a2, and a3, of three types, A1, A2, and A3. The expanded call then looks as follows:

processValues(std::forward<A1>(a1),
              std::forward<A2>(a2),
              std::forward<A3>(a3));

Inside the body of a function using a parameter pack, you can retrieve the number of arguments in the pack as follows:

int numOfArgs = sizeof...(args);

A practical example of using variadic templates is to write a secure and type-safe printf()-like function template. This would be a good practice exercise for you to try.

Variable Number of Mixin Classes

Parameter packs can be used almost everywhere. For example, the following code uses a parameter pack to define a variable number of mixin classes for MyClass. Chapter 5 discusses the concept of mixin classes.

class Mixin1
{
    public:
        Mixin1(int i) : mValue(i) {}
        virtual void Mixin1Func() { cout << "Mixin1: " << mValue << endl; }
    private:
        int mValue;
};

class Mixin2
{
    public:
        Mixin2(int i) : mValue(i) {}
        virtual void Mixin2Func() { cout << "Mixin2: " << mValue << endl; }
    private:
        int mValue;
};

template<typename... Mixins>
class MyClass : public Mixins...
{
    public:
        MyClass(const Mixins&... mixins) : Mixins(mixins)... {}
        virtual ~MyClass() = default;
};

This code first defines two mixin classes: Mixin1 and Mixin2. They are kept pretty simple for this example. Their constructor accepts an integer, which is stored, and they have a function to print information about that specific instance of the class. The MyClass variadic template uses a parameter pack typename... Mixins to accept a variable number of mixin classes. The class then inherits from all those mixin classes and the constructor accepts the same number of arguments to initialize each inherited mixin class. Remember that the ... expansion operator basically takes what is on the left of the operator and repeats it for every template parameter in the pack, separated by commas. The class can be used as follows:

MyClass<Mixin1, Mixin2> a(Mixin1(11), Mixin2(22));
a.Mixin1Func();
a.Mixin2Func();

MyClass<Mixin1> b(Mixin1(33));
b.Mixin1Func();
//b.Mixin2Func();    // Error: does not compile.

MyClass<> c;
//c.Mixin1Func();    // Error: does not compile.
//c.Mixin2Func();    // Error: does not compile.

When you try to call Mixin2Func() on b, you will get a compilation error because b is not inheriting from the Mixin2 class. The output of this program is as follows:

Mixin1: 11
Mixin2: 22
Mixin1: 33

image Folding Expressions

C++17 adds supports for so-called folding expressions. This makes working with parameter packs in variadic templates much easier. The following table lists the four types of folds that are supported. In this table, Ѳ can be any of the following operators: + - * / % ^ & | << >> += -= *= /= %= ^= &= |= <<= >>= = == != < > <= >= && || , .* ->*.

NAME EXPRESSION IS EXPANDED TO
Unary right fold (pack Ѳ ...) pack0 Ѳ (... Ѳ (packn-1 Ѳ packn))
Unary left fold (... Ѳ pack) ((pack0 Ѳ pack1) Ѳ ... ) Ѳ packn
Binary right fold (pack Ѳ ... Ѳ Init) pack0 Ѳ (... Ѳ (packn-1 Ѳ (packn Ѳ Init)))
Binary left fold (Init Ѳ … Ѳ pack) (((Init Ѳ pack0) Ѳ pack1) Ѳ … ) Ѳ packn

Let’s look at some examples. Earlier, the processValue() function template was defined recursively as follows:

void processValues() { /* Nothing to do in this base case.*/ }

template<typename T1, typename... Tn>
void processValues(T1 arg1, Tn... args)
{
    handleValue(arg1);
    processValues(args...);
}

Because it was defined recursively, it needs a base case to stop the recursion. With folding expressions, this can be implemented with a single function template using a unary right fold, where no base case is needed:

template<typename... Tn>
void processValues(const Tn&... args)
{
    (handleValue(args), ...);
}

Basically, the three dots in the function body trigger folding. That line is expanded to call handleValue() for each argument in the parameter pack, and each call to handleValue() is separated by a comma. For example, suppose args is a parameter pack with three arguments, a1, a2, and a3. The expansion of the unary right fold then becomes as follows:

(handleValue(a1), (handleValue(a2), handleValue(a3)));

Here is another example. The printValues() function template writes all its arguments to the console, separated by newlines.

template<typename... Values>
void printValues(const Values&... values)
{
    ((cout << values << endl), ...);
}

Suppose that values is a parameter pack with three arguments, v1, v2, and v3. The expansion of the unary right fold then becomes as follows:

((cout << v1 << endl), ((cout << v2 << endl), (cout << v3 << endl)));

You can call printValues() with as many arguments as you want, as shown here:

printValues(1, "test", 2.34);

In these examples, the folding is used with the comma operator, but it can be used with almost any kind of operator. For example, the following code defines a variadic function template using a binary left fold to calculate the sum of all the values given to it. A binary left fold always requires an Init value (see the overview table earlier). So, sumValues() has two template type parameters: a normal one to specify the type of Init, and a parameter pack which can accept 0 or more arguments.

template<typename T, typename... Values>
double sumValues(const T& init, const Values&... values)
{
    return (init + ... + values);
}

Suppose that values is a parameter pack with three arguments, v1, v2, and v3. Here is the expansion of the binary left fold in that case:

return (((init + v1) + v2) + v3);

The sumValues() function template can be used as follows:

cout << sumValues(1, 2, 3.3) << endl;
cout << sumValues(1) << endl;

The template requires at least one argument, so the following does not compile:

cout << sumValues() << endl;

METAPROGRAMMING

This section touches on template metaprogramming. It is a very complicated subject and there are books written about it explaining all the little details. This book doesn’t have the space to go into all of these details. Instead, this section explains the most important concepts, with the aid of a couple of examples.

The goal of template metaprogramming is to perform some computation at compile time instead of at run time. It is basically a programming language on top of C++. The following section starts the discussion with a simple example that calculates the factorial of a number at compile time and makes the result available as a simple constant at run time.

Factorial at Compile Time

Template metaprogramming allows you to perform calculations at compile time instead of at run time. The following code is a small example that calculates the factorial of a number at compile time. The code uses template recursion, explained earlier in this chapter, which requires a recursive template and a base template to stop the recursion. By mathematical definition, the factorial of 0 is 1, so that is used as the base case.

template<unsigned char f>
class Factorial
{
    public:
        static const unsigned long long val = (f * Factorial<f - 1>::val);
};

template<>
class Factorial<0>
{
    public:
        static const unsigned long long val = 1;
};

int main()
{
    cout << Factorial<6>::val << endl;
    return 0;
}

This calculates the factorial of 6, mathematically written as 6!, which is 1×2×3×4×5×6 or 720.

For this specific example of calculating the factorial of a number at compile time, you don’t necessarily need to use template metaprogramming. Since the introduction of constexpr, it can be written as follows without any templates, though the template implementation still serves as a good example on how to implement recursive templates.

constexpr unsigned long long factorial(unsigned char f)
{
    if (f == 0) {
        return 1;
    } else {
        return f * factorial(f - 1);
    }
}

If you call this version as follows, the value is calculated at compile time:

constexpr auto f1 = factorial(6);

However, you have to be careful not to forget the constexpr in this statement. If, instead, you write the following, then the calculation happens at run time!

auto f1 = factorial(6);

You cannot make such a mistake with the template metaprogramming version; that one always happens at compile time.

Loop Unrolling

A second example of template metaprogramming is to unroll loops at compile time instead of executing the loop at run time. Note that loop unrolling should only be done when you really need it, because the compiler is usually smart enough to unroll loops that can be unrolled for you.

This example again uses template recursion because it needs to do something in a loop at compile time. On each recursion, the Loop template instantiates itself with i-1. When it hits 0, the recursion stops.

template<int i>
class Loop
{
    public:
        template <typename FuncType>
        static inline void Do(FuncType func) {
            Loop<i - 1>::Do(func);
            func(i);
        }
};

template<>
class Loop<0>
{
    public:
        template <typename FuncType>
        static inline void Do(FuncType /* func */) { }
};

The Loop template can be used as follows:

void DoWork(int i) { cout << "DoWork(" << i << ")" << endl; }

int main()
{
    Loop<3>::Do(DoWork);
}

This code causes the compiler to unroll the loop and to call the function DoWork() three times in a row. The output of the program is as follows:

DoWork(1)
DoWork(2)
DoWork(3)

With a lambda expression you can use a version of DoWork() that accepts more than one parameter:

void DoWork2(string str, int i)
{
    cout << "DoWork2(" << str << ", " << i << ")" << endl;
}

int main()
{
    Loop<2>::Do([](int i) { DoWork2("TestStr", i); });
}

The code first implements a function that accepts a string and an int. The main() function uses a lambda expression to call DoWork2() on each iteration with a fixed string, "TestStr", as first argument. If you compile and run this code, the output is as follows:

DoWork2(TestStr, 1)
DoWork2(TestStr, 2)

Printing Tuples

This example uses template metaprogramming to print the individual elements of an std::tuple. Tuples are explained in Chapter 20. They allow you to store any number of values, each with its own specific type. A tuple has a fixed size and fixed value types, determined at compile time. However, tuples don’t have any built-in mechanism to iterate over their elements. The following example shows how you could use template metaprogramming to iterate over the elements of a tuple at compile time.

As is often the case with template metaprogramming, this example is again using template recursion. The tuple_print class template has two template parameters: the tuple type, and an integer, initialized with the size of the tuple. It then recursively instantiates itself in the constructor and decrements the integer on every call. A partial specialization of tuple_print stops the recursion when this integer hits 0. The main() function shows how this tuple_print class template can be used.

template<typename TupleType, int n>
class tuple_print
{
    public:
        tuple_print(const TupleType& t) {
            tuple_print<TupleType, n - 1> tp(t);
            cout << get<n - 1>(t) << endl;
        }
};

template<typename TupleType>
class tuple_print<TupleType, 0>
{
    public:
        tuple_print(const TupleType&) { }
};

int main()
{
    using MyTuple = tuple<int, string, bool>;
    MyTuple t1(16, "Test", true);
    tuple_print<MyTuple, tuple_size<MyTuple>::value> tp(t1);
}

If you look at the main() function, you can see that the line to use the tuple_print template looks a bit complicated because it requires the exact type of the tuple and the size of the tuple as template arguments. This can be simplified a lot by introducing a helper function template that automatically deduces the template parameters. The simplified implementation is as follows:

template<typename TupleType, int n>
class tuple_print_helper
{
    public:
        tuple_print_helper(const TupleType& t) {
            tuple_print_helper<TupleType, n - 1> tp(t);
            cout << get<n - 1>(t) << endl;
        }
};

template<typename TupleType>
class tuple_print_helper<TupleType, 0>
{
    public:
        tuple_print_helper(const TupleType&) { }
};

template<typename T>
void tuple_print(const T& t)
{
    tuple_print_helper<T, tuple_size<T>::value> tph(t);
}

int main()
{
    auto t1 = make_tuple(167, "Testing", false, 2.3);
    tuple_print(t1);
}

The first change made here is renaming the original tuple_print class template to tuple_print_helper. The code then implements a small function template called tuple_print(). It accepts the tuple’s type as a template type parameter, and accepts a reference to the tuple itself as a function parameter. The body of that function instantiates the tuple_print_helper class template. The main() function shows how to use this simplified version. Because you no longer need to know the exact type of the tuple, you can use make_tuple() together with auto. The call to the tuple_print() function template is very simple:

tuple_print(t1);

You don’t need to specify the function template parameter because the compiler can deduce this automatically from the supplied argument.

image constexpr if

C++17 introduced constexpr if. These are if statements executed at compile time, not at run time. If a branch of a constexpr if statement is never taken, it is never compiled. This can be used to simplify a lot of template metaprogramming techniques, and also comes in handy for SFINAE (discussed later in this chapter).

For example, you can simplify the previous code for printing elements of a tuple using constexpr if, as follows. Note that the template recursion base case is not needed anymore, because the recursion is stopped with the constexpr if statement.

template<typename TupleType, int n>
class tuple_print_helper
{
    public:
        tuple_print_helper(const TupleType& t) {
            if constexpr(n > 1) {
                tuple_print_helper<TupleType, n - 1> tp(t);
            }
            cout << get<n - 1>(t) << endl;
        }
};

template<typename T>
void tuple_print(const T& t)
{
    tuple_print_helper<T, tuple_size<T>::value> tph(t);
}

Now you can even get rid of the class template itself, and replace it with a simple function template called tuple_print_helper:

template<typename TupleType, int n>
void tuple_print_helper(const TupleType& t) {
    if constexpr(n > 1) {
        tuple_print_helper<TupleType, n - 1>(t);
    }
    cout << get<n - 1>(t) << endl;
}

template<typename T>
void tuple_print(const T& t)
{
    tuple_print_helper<T, tuple_size<T>::value>(t);
}

This can be simplified even more. Both methods can be combined into one, as follows:

template<typename TupleType, int n = tuple_size<TupleType>::value>
void tuple_print(const TupleType& t) {
    if constexpr(n > 1) {
        tuple_print<TupleType, n - 1>(t);
    }
    cout << get<n - 1>(t) << endl;
}

It can still be called the same as before:

auto t1 = make_tuple(167, "Testing", false, 2.3);
tuple_print(t1);

Using a Compile-Time Integer Sequence with Folding

C++ supports compile-time integer sequences using std::integer_sequence, which is defined in <utility>. A common use case with template metaprogramming is to generate a compile-time sequence of indices, that is, an integer sequence of type size_t. For this, a helper std::index_sequence is available. You can use std::index_sequence_for to generate an index sequence of the same length as the length of a given parameter pack.

The tuple printer could be implemented using variadic templates, compile-time index sequences, and C++17 folding expressions as follows:

template<typename Tuple, size_t... Indices>
void tuple_print_helper(const Tuple& t, index_sequence<Indices...>)
{
    ((cout << get<Indices>(t) << endl), ...);
}

template<typename... Args>
void tuple_print(const tuple<Args...>& t)
{
    tuple_print_helper(t, index_sequence_for<Args...>());
}

It can be called in the same way as before:

auto t1 = make_tuple(167, "Testing", false, 2.3);
tuple_print(t1);

With this call, the unary right fold expression in the tuple_print_helper() function template expands to the following:

(((cout << get<0>(t) << endl),
 ((cout << get<1>(t) << endl),
 ((cout << get<2>(t) << endl),
  (cout << get<3>(t) << endl)))));

Type Traits

Type traits allow you to make decisions based on types at compile time. For example, you can write a template that requires a type that is derived from a certain type, or a type that is convertible to a certain type, or a type that is integral, and so on. The C++ standard defines several helper classes for this. All type traits-related functionality is defined in the <type_traits> header file. Type traits are divided into separate categories. The following list gives a few examples of the available type traits in each category. Consult a Standard Library Reference, see Appendix B, for a complete list.

  • Primary type categories
    • is_void
    • is_integral
    • is_floating_point
    • is_pointer
    • ...
  • Type properties
    • is_const
    • is_literal_type
    • is_polymorphic
    • is_unsigned
    • is_constructible
    • is_copy_constructible
    • is_move_constructible
    • is_assignable
    • is_trivially_copyable
    • is_swappable*
    • is_nothrow_swappable*
    • has_virtual_destructor
    • has_unique_object_representations*
    • ...
  • Reference modifications
    • remove_reference
    • add_lvalue_reference
    • add_rvalue_reference
  • Pointer modifications
    • remove_pointer
    • add_pointer
  • Composited type categories
    • is_reference
    • is_object
    • is_scalar
    • ...
  • Type relations
    • is_same
    • is_base_of
    • is_convertible
    • is_invocable*
    • is_nothrow_invocable*
    • ...
  • const-volatile modifications
    • remove_const
    • add_const
    • ...
  • Sign modifications
    • make_signed
    • make_unsigned
  • Array modifications
    • remove_extent
    • remove_all_extents
  • Logical operator traits
    • conjuction*
    • disjunction*
    • negation*
  • Other transformations
    • enable_if
    • conditional
    • invoke_result*
    • ...

The type traits marked with an asterisk (*) are only available since C++17.

Type traits is a pretty advanced C++ feature. By just looking at the preceding list, which is already a shortened version of the list from the C++ standard, it is clear that this book cannot explain all details about all type traits. This section explains just a couple of use cases to show you how type traits can be used.

Using Type Categories

Before an example can be given for a template using type traits, you first need to know a bit more on how classes like is_integral work. The C++ standard defines an integral_constant class that looks like this:

template <class T, T v>
struct integral_constant {
    static constexpr T value = v;
    using value_type = T;
    using type = integral_constant<T, v>;
    constexpr operator value_type() const noexcept { return value; }
    constexpr value_type operator()() const noexcept { return value; }
};

It also defines bool_constant, true_type, and false_type type aliases:

template <bool B>
using bool_constant = integral_constant<bool, B>;

using true_type = bool_constant<true>;
using false_type = bool_constant<false>;

What this code defines is two types: true_type and false_type. When you access true_type::value, you get the value true, and when you access false_type::value, you get the value false. You can also access true_type::type, which results in the type of true_type. The same holds for false_type. Classes like is_integral and is_class inherit from either true_type or false_type. For example, is_integral can be specialized for type bool as follows:

template<> struct is_integral<bool> : public true_type { };

This allows you to write is_integral<bool>::value, which results in the value true. Note that you don’t need to write these specializations yourself; they are part of the Standard Library.

The following code shows the simplest example of how type categories can be used:

if (is_integral<int>::value) {
    cout << "int is integral" << endl;
} else {
    cout << "int is not integral" << endl;
}

if (is_class<string>::value) {
    cout << "string is a class" << endl;
} else {
    cout << "string is not a class" << endl;
}

This example uses is_integral to check whether or not int is an integral type, and it uses is_class to check whether or not string is a class. The output is as follows:

int is integral
string is a class

image For each trait that has a value member, C++17 adds a variable template that has the same name as the trait followed by _v. Instead of writing some_trait<T>::value, you can write some_trait_v<T>—for example, is_integral_v<T>, is_const_v<T>, and so on. Here is the previous example written using these helpers:

if (is_integral_v<int>) {
    cout << "int is integral" << endl;
} else {
    cout << "int is not integral" << endl;
}

if (is_class_v<string>) {
    cout << "string is a class" << endl;
} else {
    cout << "string is not a class" << endl;
}

Of course, you will likely never use type traits in this way. They become more useful in combination with templates to generate code based on some properties of a type. The following template functions demonstrate this. The code defines two overloaded process_helper() function templates that accept a type as template parameter. The first parameter to these functions is a value, and the second is an instance of either true_type or false_type. The process() function template accepts a single parameter and calls process_helper().

template<typename T>
void process_helper(const T& t, true_type)
{
    cout << t << " is an integral type." << endl;
}

template<typename T>
void process_helper(const T& t, false_type)
{
    cout << t << " is a non-integral type." << endl;
}

template<typename T>
void process(const T& t)
{
    process_helper(t, typename is_integral<T>::type());
}

The second argument in the call to process_helper() is as follows:

typename is_integral<T>::type()

This argument uses is_integral to figure out if T is an integral type. You use ::type to access the resulting integral_constant type, which can be true_type or false_type. The process_helper() function needs an instance of true_type or false_type as a second parameter, so that is the reason for the two empty parentheses behind ::type. Note that the two overloaded process_helper() functions use nameless parameters of type true_type and false_type. They are nameless because they don’t use those parameters inside their function body. These parameters are only used for function overload resolution.

The code can be tested as follows:

process(123);
process(2.2);
process("Test"s);

Here is the output:

123 is an integral type.
2.2 is a non-integral type.
Test is a non-integral type.

The previous example could be written as a single function template as follows. However, that doesn’t demonstrate how to use type traits to select different overloads based on a type.

template<typename T>
void process(const T& t)
{
    if constexpr (is_integral_v<T>) {
        cout << t << " is an integral type." << endl;
    } else {
        cout << t << " is a non-integral type." << endl;
    }
}

Using Type Relations

Some examples of type relations are is_same, is_base_of, and is_convertible. This section gives an example of how to use is_same; the other type relations work similarly.

The following same() function template uses the is_same type trait to figure out whether or not the two given arguments are of the same type, and outputs an appropriate message:

template<typename T1, typename T2>
void same(const T1& t1, const T2& t2)
{
    bool areTypesTheSame = is_same_v<T1, T2>;
    cout << "'" << t1 << "' and '" << t2 << "' are ";
    cout << (areTypesTheSame ? "the same types." : "different types.") << endl;
}

int main()
{
    same(1, 32);
    same(1, 3.01);
    same(3.01, "Test"s);
}

The output is as follows:

'1' and '32' are the same types.
'1' and '3.01' are different types
'3.01' and 'Test' are different types

Using enable_if

The use of enable_if is based on a feature called Substitution Failure Is Not An Error (SFINAE), a complicated feature of C++. This section only explains the basics of SFINAE.

If you have a set of overloaded functions, you can use enable_if to selectively disable certain overloads based on some type traits. The enable_if trait is usually used on the return types for your set of overloads. enable_if accepts two template type parameters. The first is a Boolean, and the second is a type that is void by default. If the Boolean is true, then the enable_if class has a nested type that you can access using ::type. The type of this nested type is the type given as a second template type parameter. If the Boolean is false, then there is no nested type.

The C++ standard defines alias templates for traits that have a type member, such as enable_if. These have the same name as the trait, but are appended with _t. For example, instead of writing the following,

typename enable_if<..., bool>::type

you can write a much shorter version,

enable_if_t<..., bool>

The same() function template from the previous section can be rewritten into an overloaded check_type() function template by using enable_if as follows. In this version, the check_type() functions return true or false depending on whether or not the types of the given values are the same. If you don’t want to return anything from check_type(), you can remove the return statements, and remove the second template type parameter for enable_if or replace it with void.

template<typename T1, typename T2>
enable_if_t<is_same_v<T1, T2>, bool>
    check_type(const T1& t1, const T2& t2)
{
    cout << "'" << t1 << "' and '" << t2 << "' ";
    cout << "are the same types." << endl;
    return true;
}

template<typename T1, typename T2>
enable_if_t<!is_same_v<T1, T2>, bool>
    check_type(const T1& t1, const T2& t2)
{
    cout << "'" << t1 << "' and '" << t2 << "' ";
    cout << "are different types." << endl;
    return false;
} 

int main()
{
    check_type(1, 32);
    check_type(1, 3.01);
    check_type(3.01, "Test"s);
}

The output is the same as before:

'1' and '32' are the same types.
'1' and '3.01' are different types.
'3.01' and 'Test' are different types.

The code defines two versions of check_type(). The return type of both versions is the nested type of enable_if, which is bool. First, is_same_v is used to check whether or not the two types are the same. The result is given to enable_if_t. When the first argument to enable_if_t is true, enable_if_t has type bool; otherwise, there is no type. This is where SFINAE comes into play.

When the compiler starts to compile the first line of main(), it tries to find a function check_type() that accepts two integer values. It finds the first check_type() function template overload in the source code and deduces that it can use an instance of this function template by making T1 and T2 both integers. It then tries to figure out the return type. Because both arguments are integers and thus the same types, is_same_v<T1, T2> is true, which causes enable_if_t<true, bool> to be type bool. With this instantiation, everything is fine and the compiler can use that version of check_type().

However, when the compiler tries to compile the second line in main(), it again tries to find a suitable check_type() function. It starts with the first check_type() and decides it can use that overload by setting T1 to type int and T2 to type double. It then tries to figure out the return type. This time, T1 and T2 are different types, which means that is_same_v<T1, T2> is false. Because of this, enable_if_t<false, bool> does not represent a type, leaving the function check_type() without a return type. The compiler notices this error but does not yet generate a real compilation error because of SFINAE (Substitution Failure Is Not An Error). Instead, the compiler gracefully backtracks and tries to find another check_type() function. In this case, the second check_type() works out perfectly fine because !is_same_v<T1, T2> is true, and thus enable_if_t<true, bool> is type bool.

If you want to use enable_if on a set of constructors, you can’t use it with the return type because constructors don’t have a return type. In that case, you can use enable_if on an extra constructor parameter with a default value.

It is recommended to use enable_if judiciously. Use it only when you need to resolve overload ambiguities that you cannot possibly resolve using any other technique, such as specialization, partial specialization, and so on. For example, if you just want compilation to fail when you use a template with the wrong types, use static_assert(), explained in Chapter 27, and not SFINAE. Of course, there are legitimate use cases for enable_if. One such example is specializing a copy function for a custom vector-like class to perform bit-wise copying of trivially copyable types using enable_if and the is_trivially_copyable type trait. Such a specialized copy function could for example use memcpy().

image Using constexpr if to Simplify enable_if Constructs

As you can see from the earlier examples, using enable_if can become quite complicated. The constexpr if feature, introduced in C++17, helps to dramatically simplify certain use cases of enable_if.

For example, suppose you have the following two classes:

class IsDoable
{
    public:
        void doit() const { cout << "IsDoable::doit()" << endl; }
};

class Derived : public IsDoable { };

You can create a function template, call_doit(), that calls the doit() method if the method is available; otherwise, it prints an error message to the console. You can do this with enable_if by checking whether the given type is derived from IsDoable:

template<typename T>
enable_if_t<is_base_of_v<IsDoable, T>, void>
    call_doit(const T& t)
{
    t.doit();
}

template<typename T>
enable_if_t<!is_base_of_v<IsDoable, T>, void>
    call_doit(const T&)
{
    cout << "Cannot call doit()!" << endl;
}

The following code tests this implementation:

Derived d;
call_doit(d);
call_doit(123);

Here is the output:

IsDoable::doit()
Cannot call doit()!

You can simplify this enable_if implementation a lot by using C++17 constexpr if:

template<typename T>
void call_doit(const T& [[maybe_unused]] t)
{
    if constexpr(is_base_of_v<IsDoable, T>) {
        t.doit();
    } else {
        cout << "Cannot call doit()!" << endl;
    }
}

You cannot accomplish this using a normal if statement! With a normal if statement, both branches need to be compiled, and this will fail if you supply a type T that is not derived from IsDoable. In that case, the line t.doit() will fail to compile. However, with the constexpr if statement, if a type is supplied that is not derived from IsDoable, then the line t.doit() won’t even be compiled!

Note also the use of the [[maybe_unused]] attribute, introduced in C++17. Because the line t.doit() is not compiled if the given type T is not derived from IsDoable, the parameter t won’t be used in that instantiation of call_doit(). Most compilers give a warning or even an error if you have unused parameters. The attribute prevents such warnings or errors for the parameter t.

Instead of using the is_base_of type trait, you can also use the new C++17 is_invocable trait, which determines whether or not a given function can be called with a given set of arguments. Here is an implementation of call_doit() using the is_invocable trait:

template<typename T>
void call_doit(const T& [[maybe_unused]] t)
{
    if constexpr(is_invocable_v<decltype(&IsDoable::doit), T>) {
        t.doit();
    } else {
        cout << "Cannot call doit()!" << endl;
    }
}

image Logical Operator Traits

There are three logical operator traits: conjunction, disjunction, and negation. Variable templates, ending with _v, are available as well. These traits accept a variable number of template type parameters, and can be used to perform logical operations on type traits, as in this example:

cout << conjunction_v<is_integral<int>, is_integral<short>> << " ";
cout << conjunction_v<is_integral<int>, is_integral<double>> << " ";

cout << disjunction_v<is_integral<int>, is_integral<double>,
                      is_integral<short>> << " ";

cout << negation_v<is_integral<int>> << " ";

The output is as follows:

1 0 1 0

Metaprogramming Conclusion

As you have seen in this section, template metaprogramming can be a very powerful tool, but it can also get quite complicated. One problem with template metaprogramming, not mentioned before, is that everything happens at compile time so you cannot use a debugger to pinpoint a problem. If you decide to use template metaprogramming in your code, make sure you write good comments to explain exactly what is going on and why you are doing something a certain way. If you don’t properly document your template metaprogramming code, it might be very difficult for someone else to understand your code, and it might even make it difficult for you to understand your own code in the future.

SUMMARY

This chapter is a continuation of the template discussion from Chapter 12. These chapters show you how to use templates for generic programming, and template metaprogramming for compile-time computations. Hopefully you gained an appreciation for the power and capabilities of these features, and an idea of how you could apply these concepts to your own code. Don’t worry if you didn’t understand all the syntax, or didn’t follow all the examples, on your first reading. The concepts can be difficult to grasp when you are first exposed to them, and the syntax is tricky whenever you want to write more complicated templates. When you actually sit down to write a class or function template, you can consult this chapter and Chapter 12 for a reference on the proper syntax.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset