Advanced Templates - Mastering Advanced Features of C++ - Professional C++ (2014)

Professional C++ (2014)

Part IVMastering Advanced Features of C++

Chapter 21Advanced Templates

WHAT’S IN THIS CHAPTER?

· The different kinds of template parameters

· How to use partial specialization

· How to write recursive templates

· Explaining variadic templates

· How to write type-safe variable argument functions using variadic templates

· What metaprogramming is and how to use it

WROX.COM DOWNLOADS FOR THIS CHAPTER

Please note that all the code examples for this chapter are available as a part of this chapter’s code download on the book’s website at www.wrox.com/go/proc++3e on the Download Code tab.

Chapter 11 covered the most widely used features of class and function templates. If you are interested in only a basic knowledge of templates so that you can better understand how the STL works, or perhaps write your own simple classes, you can skip this chapter on advanced templates. However, if templates interest you and you want to uncover their full power, continue reading this chapter to learn about some of the more obscure, but fascinating, details.

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). You’ve seen examples of type and non-type parameters in Chapter 11, but not template template parameters yet. There are also some tricky aspects to both type and non-type parameters that are not covered in Chapter 11.

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 11 a second type parameter specifying another templatized class container on which to build the grid. The standard template 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 she wants 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 inWidth = kDefaultWidth,

size_t inHeight = kDefaultHeight);

virtual ~Grid();

// Sets an element at a given location. The element is copied.

void setElementAt(size_t x, size_t y, const T& inElem);

T& getElementAt(size_t x, size_t y);

const T& getElementAt(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 initializeCellsContainer();

std::vector<Container> mCells;

size_t mWidth, mHeight;

};

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. The only other change is that mCells is now a vector of Containers instead of avector of vectors.

Here is the constructor definition:

template <typename T, typename Container>

Grid<T, Container>::Grid(size_t inWidth, size_t inHeight) :

mWidth(inWidth), mHeight(inHeight)

{

initializeCellsContainer();

}

The constructor delegates the work to initializeCellsContainer(), which is implemented as follows. It 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.

template <typename T, typename Container>

void Grid<T, Container>::initializeCellsContainer()

{

mCells.resize(mWidth);

for (auto& column : mCells) {

column.resize(mHeight);

}

}

Here are the implementations of the remaining methods:

template <typename T, typename Container>

void Grid<T, Container>::setElementAt(size_t x, size_t y, const T& inElem)

{

mCells[x][y] = inElem;

}

template <typename T, typename Container>

T& Grid<T, Container>::getElementAt(size_t x, size_t y)

{

return mCells[x][y];

}

template <typename T, typename Container>

const T& Grid<T, Container>::getElementAt(size_t x, size_t y) const

{

return mCells[x][y];

}

Now you can instantiate and use Grid objects like this:

Grid<int, vector<int>> myIntVectorGrid;

Grid<int, deque<int>> myIntDequeGrid;

myIntVectorGrid.setElementAt(3, 4, 5);

cout << myIntVectorGrid.getElementAt(3, 4) << endl;

myIntDequeGrid.setElementAt(1, 2, 3);

cout << myIntDequeGrid.getElementAt(1, 2) << endl;

Grid<int, vector<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 will not compile, but it might not give you the error you expect. It won’t complain that the second type argument is an int instead of a container. It will tell you something more cryptic. For example, Microsoft Visual C++ tells you that left of '.resize' must have class/struct/union type. That’s because the compiler attempts to generate a Grid class with int as the Container. Everything works fine until it tries to compile this line:

column.resize(mHeight);

At that point, the compiler realizes that column is an int, so you can’t call resize() on it.

This approach is used in the STL. The stack, queue, and priority_queue class templates all take a template type parameter specifying the underlying container.

Default Values for Template Type 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<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 vector 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 parameter, clients can now instantiate a grid with or without specifying an underlying container:

Grid<int, deque<int>> myDequeGrid;

Grid<int, vector<int>> myVectorGrid;

Grid<int> myVectorGrid2;

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<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 vector. What if you wrote this instead?

Grid<int, vector<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 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 as follows. The E parameter is the element type. The Allocator parameter is covered in Chapter 16.

template <typename E, typename Allocator = allocator<E> >

class vector

{

// Vector definition

};

To pass such a container as a template template parameter, all you have to do is copy-paste the declaration of the class template (in this example: template <typename E, typename Allocator = 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

private:

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

// Omitted code that is the same as before

};

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 is 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<T> as the container type. For example, the declaration of mCells now is as follows:

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

After implementing all the methods, you can use the template as follows:

Grid<int, vector> myGrid;

myGrid.setElementAt(1, 2, 3);

cout << myGrid.getElementAt(1, 2) << 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>

void Grid<T, DEFAULT>::initializeCellsContainer()

{

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 then instantiate an int Grid 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.

WARNING Non-type parameters cannot be objects, or even doubles or floats. They are restricted to integral types, enums, pointers, and references.

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

Reference and Pointer Non-Type Template Parameters

A more comprehensive way of allowing the user to specify an initial element value for the 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. However, the C++ standard says that the reference you pass as the second template argument must be a constant expression, and must be a reference to a complete object with static storage duration and external or internal linkage. Chapter 10 discusses external and internal linkage.

Here is a program that declares int and SpreadsheetCell grids with initial values.

namespace {

const int defaultInt = 11;

const SpreadsheetCell defaultCell(1.2);

}

int main()

{

Grid<int, defaultInt> myIntGrid;

Grid<SpreadsheetCell, defaultCell> mySpreadsheet;

Grid<int, defaultInt> myIntGrid2(myIntGrid);

return 0;

}

CLASS TEMPLATE PARTIAL SPECIALIZATION

The const char* class specialization shown in Chapter 11 is called 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();

virtual ~Grid();

void setElementAt(size_t x, size_t y, const T& inElem);

T& getElementAt(size_t x, size_t y);

const T& getElementAt(size_t x, size_t y) const;

size_t getHeight() const { return HEIGHT; }

size_t getWidth() const { return WIDTH; }

private:

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();

virtual ~Grid();

void setElementAt(size_t x, size_t y, const char* inElem);

const char* getElementAt(size_t x, size_t y) const;

size_t getHeight() const { return HEIGHT; }

size_t getWidth() const { return WIDTH; }

private:

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 char* Grid<const char*, WIDTH, HEIGHT>::getElementAt(

size_t x, size_t y) const

{

return mCells[x][y].c_str();

}

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>.

Another Form of Partial Specialization

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 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 pointers, so it automatically frees the memory when necessary:

#include "Grid.h"

#include <memory>

template <typename T>

class Grid<T*>

{

public:

explicit Grid(size_t inWidth = kDefaultWidth,

size_t inHeight = kDefaultHeight);

Grid(const Grid<T*>& src);

virtual ~Grid();

Grid<T*>& operator=(const Grid<T*>& rhs);

// The Grid becomes the owner of the element!

void setElementAt(size_t x, size_t y, std::unique_ptr<T> inElem);

T* getElementAt(size_t x, size_t y);

const T* getElementAt(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 initializeCellsContainer();

void copyFrom(const Grid<T*>& src);

std::vector<std::vector<std::unique_ptr<T>>> mCells;

size_t mWidth, mHeight;

};

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 this: Grid<int*> myIntGrid, then T will actually be int, notint*. That’s a bit unintuitive, but unfortunately, the way it works. Here is a code example:

Grid<int> myIntGrid; // Uses the non-specialized grid

Grid<int*> psGrid(2, 2); // Uses the partial specialization for pointer types

psGrid.setElementAt(0, 0, make_unique<int>(1));

psGrid.setElementAt(0, 1, make_unique<int>(2));

psGrid.setElementAt(1, 0, make_unique<int>(3));

Grid<int*> psGrid2(psGrid);

Grid<int*> psGrid3;

psGrid3 = psGrid2;

auto element = psGrid2.getElementAt(1, 0);

if (element) {

cout << *element << endl;

*element = 6;

}

cout << *psGrid.getElementAt(1, 0) << endl; // psGrid is not modified

cout << *psGrid2.getElementAt(1, 0) << endl; // psGrid2 is modified

The output is:

3

3

6

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

template <typename T>

void Grid<T*>::copyFrom(const Grid<T*>& src)

{

mWidth = src.mWidth;

mHeight = src.mHeight;

initializeCellsContainer();

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 11, 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*& value, T** 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 otherwise it won’t compile.

template <typename T>

size_t Find(T*& value, T** 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*&. This is done to make it symmetric with the original Find() function template, which accepts a T& as a first parameter. However, in this case, using T* instead of T*& for the first parameter of the partial specialization of Find() works as well.

More on Deduction

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.

NOTE Between all overloaded versions, function template specializations, and specific function template instantiations, the compiler always chooses the “most specific” one to call. If a non-template version is equally specific as a function template instantiation, then the compiler prefers the non-template version.

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

size_t res = NOT_FOUND;

int x = 3, intArr[] = {1, 2, 3, 4};

size_t sizeArr = sizeof(intArr) / sizeof(int);

res = Find(x, intArr, sizeArr); // calls Find<int> by deduction

res = Find<int>(x, intArr, sizeArr); // calls Find<int> explicitly

double d1 = 5.6, dArr[] = {1.2, 3.4, 5.7, 7.5};

sizeArr = sizeof(dArr) / sizeof(double);

res = Find(d1, dArr, sizeArr); // calls Find<double> by deduction

res = Find<double>(d1, dArr, sizeArr); // calls Find<double> explicitly

const char* word = "two";

const char* arr[] = {"one", "two", "three", "four"};

sizeArr = sizeof(arr) / sizeof(arr[0]);

// calls template specialization for const char*s

res = Find<const char*>(word, arr, sizeArr);

res = Find(word, arr, sizeArr); // calls overloaded Find for const char*s

int *px = &x, *pArr[] = {&x, &x};

sizeArr = sizeof(pArr) / sizeof(pArr[0]);

res = Find(px, pArr, sizeArr); // calls the overloaded Find for pointers

SpreadsheetCell c1(10), c2[] = {SpreadsheetCell(4), SpreadsheetCell(10)};

sizeArr = sizeof(c2) / sizeof(c2[0]);

res = Find(c1, c2, sizeArr); // calls Find<SpreadsheetCell> by deduction

// calls Find<SpreadsheetCell> explicitly

res = Find<SpreadsheetCell>(c1, c2, sizeArr);

SpreadsheetCell *pc1 = &c1;

SpreadsheetCell *psa[] = {&c1, &c1};

sizeArr = sizeof(psa) / sizeof(psa[0]);

res = Find(pc1, psa, sizeArr); // Calls the overloaded Find for pointers

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 11. 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 14. If you skipped that chapter or are unfamiliar with the syntax for overloading operator[], consult Chapter 14 before continuing.

An N-Dimensional Grid: First Attempt

The Grid template example up to now supports only two dimensions, which limits 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 for each of those dimensions. However, that would repeat a lot of code. Another approach is 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 setElementAt() and getElementAt():

template <typename T>

class OneDGrid

{

public:

explicit OneDGrid(size_t inSize = kDefaultSize);

virtual ~OneDGrid();

T& operator[](size_t x);

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

void resize(size_t newSize);

size_t getSize() const { return mElems.size(); }

static const size_t kDefaultSize = 10;

private:

std::vector<T> mElems;

};

template <typename T>

OneDGrid<T>::OneDGrid(size_t inSize)

{

mElems.resize(inSize);

}

template <typename T>

OneDGrid<T>::~OneDGrid()

{

// Nothing to do, the vector will clean up itself.

}

template <typename T>

void OneDGrid<T>::resize(size_t newSize)

{

mElems.resize(newSize);

}

template <typename T>

T& OneDGrid<T>::operator[](size_t x)

{

return mElems[x];

}

template <typename T>

const T& OneDGrid<T>::operator[](size_t x) const

{

return mElems[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. We 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 nesting 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. 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 inSize = kDefaultSize);

virtual ~NDGrid();

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 mElems.size(); }

static const size_t kDefaultSize = 10;

private:

std::vector<NDGrid<T, N-1>> mElems;

};

Note that mElems 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 inSize = kDefaultSize);

virtual ~NDGrid();

T& operator[](size_t x);

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

void resize(size_t newSize);

size_t getSize() const { return mElems.size(); }

static const size_t kDefaultSize = 10;

private:

std::vector<T> mElems;

};

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 main NDGrid template, with highlights showing the differences from the OneDGrid:

template <typename T, size_t N>

NDGrid<T, N>::NDGrid(size_t inSize)

{

resize(inSize);

}

template <typename T, size_t N>

NDGrid<T, N>::~NDGrid()

{

// Nothing to do, the vector will clean up itself.

}

template <typename T, size_t N>

void NDGrid<T, N>::resize(size_t newSize)

{

mElems.resize(newSize);

// Resizing the vector calls the 0-argument constructor for

// the NDGrid<T, N-1> elements, which constructs

// it 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 : mElems) {

element.resize(newSize);

}

}

template <typename T, size_t N>

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

{

return mElems[x];

}

template <typename T, size_t N>

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

{

return mElems[x];

}

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 inSize)

{

resize(inSize);

}

template <typename T>

NDGrid<T, 1>::~NDGrid()

{

// Nothing to do, the vector will clean up itself.

}

template <typename T>

void NDGrid<T, 1>::resize(size_t newSize)

{

mElems.resize(newSize);

}

template <typename T>

T& NDGrid<T, 1>::operator[](size_t x)

{

return mElems[x];

}

template <typename T>

const T& NDGrid<T, 1>::operator[](size_t x) const

{

return mElems[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;

TYPE INFERENCE

Type inference is discussed in Chapter 1 and allows the compiler to automatically deduce the exact type of an expression. There are two keywords for type inference: auto and decltype. Type inference turns out to be very useful in combination with templates. This section goes into more detail on their use in a template context.

auto and decltype with Templates

The use of the auto and decltype keywords in combination with templates is best illustrated with an example. The following example defines two classes: MyInt and MyString. These are simple wrappers for an int and a std::string, respectively. Their constructors accept a single value used for initialization. Both classes also have an operator+ as a member method. Here is the header file:

// Forward class declaration

class MyString;

class MyInt

{

public:

MyInt(int i) : mValue(i) {}

MyInt operator+(const MyString& rhs) const;

int getInt() const { return mValue; }

private:

int mValue;

};

class MyString

{

public:

MyString(const std::string& str) : mString(str) {}

MyString operator+(const MyInt& rhs) const;

const std::string& getString() const { return mString; }

private:

std::string mString;

};

The implementation is as follows. This code uses stoi() and to_string(), and both of them are discussed in the “Numeric Conversions” section in Chapter 2:

MyInt MyInt::operator+(const MyString& rhs) const

{

return mValue + stoi(rhs.getString());

}

MyString MyString::operator+(const MyInt& rhs) const

{

string str = mString;

str.append(to_string(rhs.getInt()));

return str;

}

image These operators are implemented as such to force a different result depending on the order of the arguments to the addition operator. For example:

MyInt i(4);

MyString str("5");

MyInt a = i + str;

MyString b = str + i;

In this case, the type of variable a is MyInt and the type of variable b is MyString. Now imagine that you want to write a function template to perform the addition. You can write the following:

template<typename T1, typename T2, typename Result>

Result DoAddition(const T1& t1, const T2& t2)

{

return t1 + t2;

}

As you can see, it requires you to specify three template parameters: the type of the first operand, the type of the second operand, and the type of the result of performing the addition. You can call this function template as follows. Note that you have to specify the three template type arguments. The compiler cannot deduce them from the supplied function arguments because you only supply two function arguments and there are three template type arguments.

auto c = DoAddition<MyInt, MyString, MyInt>(i, str);

This is obviously not that elegant because you need to manually specify the type of the return value. After reading about the decltype keyword, you might want to try to fix this issue as follows:

template<typename T1, typename T2>

decltype(t1 + t2) DoAddition(const T1& t1, const T2& t2)

{

return t1 + t2;

}

However, this does not work because at the time of parsing the decltype keyword, the compiler doesn’t know t1 and t2 yet. They become known further down in the function prototype. The correct solution is to combine the alternative function syntax with the decltypekeyword as shown in the following implementation:

template<typename T1, typename T2>

auto DoAddition(const T1& t1, const T2& t2) -> decltype(t1 + t2)

{

return t1 + t2;

}

With this implementation you can call DoAddition() as follows:

auto d = DoAddition(i, str);

auto e = DoAddition(str, i);

You can see that you do not need to specify any function template parameters anymore because the compiler can now deduce them based on the arguments given to DoAddition(), and can automatically figure out the type of the return value. In this example, d is of type MyInt and e is of type MyString.

image With function return type deduction, the implementation of DoAddition() can be further simplified as follows:

template<typename T1, typename T2>

auto DoAddition(const T1& t1, const T2& t2)

{

return t1 + t2;

}

VARIADIC TEMPLATES

Normal templates can take only a fixed number of template parameters. Variadic templates are templates that 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 { };

NOTE The three dots behind typename are not an error. This is the syntax to define a parameter pack for variadic templates. A parameter pack is something that can accept a variable number of arguments. You are allowed to put spaces before and after the three dots.

You can instantiate MyVariadicTemplate with any number of types. For 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. TheprocessValues() 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 andstring in this example:

void handleValue(int value) { cout << "Integer: " << value << endl; }

void handleValue(double value) { cout << "Double: " << value << endl; }

void handleValue(const string& value) { cout << "String: " << value << endl; }

template<typename T>

void processValues(T arg) // Base case

{

handleValue(arg);

}

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 behind typename in the template parameter list and behind 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 type of use of the ... operator is behind 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 of the operator, 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 template that accepts just a single template argument.

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);

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 rvalue references, discussed in Chapter 10. The following implementation uses rvalue references, and uses std::forward() to perfectly forward all parameters. If an rvalue reference is passed toprocessValues(), then it is forwarded as an rvalue reference. If an lvalue reference is passed, then it is forwarded as an lvalue reference.

template<typename T>

void processValues(T&& arg)

{

handleValue(std::forward<T>(arg));

}

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.

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 version of printf(). This would be a good exercise to practice variadic templates.

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() {}

};

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. This implementation requires you to specify at least one mixin class. 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.

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

Mixin1: 11

Mixin2: 22

Mixin1: 33

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 the details of metaprogramming. 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.

NOTE It is important to keep in mind that the factorial calculation is happening at compile time. At run time you simply access the compile-time calculated value through ::val, which is just a static constant value.

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 usually the compiler is 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. For template recursion you need the recursive implementation of the template and a base template that stops the recursion. 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:

DoWork(1)

DoWork(2)

DoWork(3)

Using std::bind() 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(bind(DoWork2, "TestStr", placeholders::_1));

}

The code first implements a function that accepts a string and an int. The main() function uses std::bind() to bind the first parameter of DoWork2() to a fixed string, "TestStr". See Chapter 17 for details on std::bind(). 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 19. 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<int n, typename TupleType>

class tuple_print

{

public:

tuple_print(const TupleType& t) {

tuple_print<n - 1, TupleType> tp(t);

cout << get<n - 1>(t) << endl;

}

};

template<typename TupleType>

class tuple_print<0, TupleType>

{

public:

tuple_print(const TupleType&) { }

};

int main()

{

using MyTuple = tuple<int, string, bool>;

MyTuple t1(16, "Test", true);

tuple_print<tuple_size<MyTuple>::value, MyTuple> 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 size of the tuple and the exact type 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<int n, typename TupleType>

class tuple_print_helper

{

public:

tuple_print_helper(const TupleType& t) {

tuple_print_helper<n - 1, TupleType> tp(t);

cout << get<n - 1>(t) << endl;

}

};

template<typename TupleType>

class tuple_print_helper<0, TupleType>

{

public:

tuple_print_helper(const TupleType&) { }

};

template<typename T>

void tuple_print(const T& t)

{

tuple_print_helper<tuple_size<T>::value, T> 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(), which accepts the type of the tuple as a template argument and accepts a reference to thetuple itself as a function argument. The body of that function instantiates the tuple_print_helper class template. The main() function shows how to use this simplified version. Because you don’t need to know the exact type of the tuple yourself anymore, you can use the recommended make_tuple() together with the auto keyword to avoid having to write the tuple type yourself. The call to the tuple_print() function template is very simple:

tuple_print(t1);

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

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 standard defines several helper classes for this. All type traits-related functionality is defined in the <type_traits> header file. The following list gives a few examples of the available type traits-related classes in the standard. Consult a Standard Library Reference — for examplehttp://www.cppreference.com/ or http://www.cplusplus.com/reference — for a complete list.

  • Primary type categories
    1. is_void
    2. is_integral
    3. is_floating_point
    4. is_pointer
    5. ...
  • Type properties
    1. is_const
    2. is_literal_type
    3. is_polymorphic
    4. is_unsigned
    5. is_constructible
    6. is_copy_constructible
    7. is_move_constructible
    8. is_assignable
    9. is_trivially_copyable
    10. ...
  • Reference modifications
    1. remove_reference
    2. add_lvalue_reference
    3. add_rvalue_reference
  • Composited type categories
    1. is_reference
    2. is_object
    3. is_scalar
    4. ...
  • Type relations
    1. is_same
    2. is_base_of
    3. is_convertible
  • const-volatile modifications
    1. remove_const
    2. add_const
    3. ...
  • Sign modifications
    1. make_signed
    2. make_unsigned
  • Other transformations
    1. enable_if
    2. conditional
    3. ...

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 standard itself, 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 standard defines an integral_constant class that looks as follows:

template <class T, T v>

struct integral_constant {

static constexpr T value = v;

typedef T value_type;

typedef integral_constant<T,v> type;

constexpr operator value_type() const noexcept { return value; }

constexpr value_type operator()() const noexcept { return value; }

};

typedef integral_constant<bool, true> true_type;

typedef integral_constant<bool, false> false_type;

What this defines is two types: true_type and false_type. When you call true_type::value you get the value true and when you call false_type::value you get the value false. You can also call true_type::type, which returns the type of true_type. The same holds forfalse_type. Classes like is_integral and is_class inherit from integral_constant. 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 returns 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 is using is_integral to check whether int is an integral type or not, and uses is_class to check whether string is a class or not. The output is as follows:

int is integral

string is a class

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 example demonstrates this. The code defines an overloaded function template process_helper() that accepts a type as template argument. The first argument to this function is a value, and the second argument is either an instance of true_type or false_type. The process() function template accepts a single argument and callsprocess_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 parameter in the call to process_helper() is:

typename is_integral<T>::type()

This uses is_integral to figure out if T is an integral type. ::type returns 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(string("Test"));

The output of this example is as follows:

123 is an integral type.

2.2 is a non-integral type.

Test is a non-integral type.

The previous example could be written using only a process() function template as follows, but that doesn’t demonstrate how to write different overloads where one parameter is either true_type or false_type.

template<typename T>

void process(const T& t)

{

if (is_integral<T>::value) {

cout << t << " is an integral type." << endl;

} else {

cout << t << " is a non-integral type." << endl;

}

}

Using Type Relations

There are three type relations available: is_same, is_base_of, and is_convertible. This section gives an example on how to use is_same; the others work similarly.

The following same() function template uses the is_same type trait to figure out whether the two given arguments are of the same type or not, and outputs an appropriate message. Using the same() function template is very easy as is shown in the main() function:

template<typename T1, typename T2>

void same(const T1& t1, const T2& t2)

{

bool areTypesTheSame = is_same<T1, T2>::value;

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, string("Test"));

}

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

First a little warning before continuing with enable_if. Using enable_if requires knowledge of a feature called Substitution Failure Is Not An Error (SFINAE), a complicated and obscure feature of C++. This section explains the basics of SFINAE with an example using enable_if.

If you have a set of overloaded functions, you can use enable_if to selectively disable certain overloads based on some type traits. enable_if 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 example from the previous section with the same() function template 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 the types of the given values are the same or not. If you don’t want to return anything from check_type() you can remove the second template type parameter for enable_if or replace it with void.

template<typename T1, typename T2>

typename enable_if<is_same<T1, T2>::value, bool>::type

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>

typename enable_if<!is_same<T1, T2>::value, bool>::type

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, string("Test"));

}

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. First, is_same is used to check whether the two types are the same or not and the result is retrieved with ::value. This value is given to enable_if, and ::type is used to get the resulting type, which is specified to be of type bool. When the first argument to enable_if is true then ::type has type bool. When the first argument to enable_if is false, then there is no wrapped type, so ::type fails. 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 by making T1and T2 both integers. It then tries to figure out the return type. Since both arguments are integers and thus the same types, is_same<T1, T2>::value returns true, which causes enable_if<true, bool>::type to be of 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 integer 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<T1, T2>::value returns false. Because of this, enable_if<false, bool> has no wrapped type and thus enable_if<false, bool>::type fails, 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. The compiler gracefully backtracks and tries to find another check_type() function. In this case the second check_type() works out perfectly because!is_same<T1, T2>::value is true and thus enable_if<true, bool>::type is of type bool.

If you want to use enable_if on a set of constructors, you can’t use it for 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 26, 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 (for example, with the C function memcpy()) of trivially copyable types using enable_if and the is_trivially_copyable type trait.

WARNING Relying on SFINAE is tricky and complicated. If your use of SFINAE and enable_if selectively disables the wrong overloads in your overload set, you will get cryptic compiler errors, which will be hard to track down.

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 yet 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 yourself to understand your own code in the future.

SUMMARY

This chapter is a continuation of the template discussion from Chapter 11. 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 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 somewhat more complicated templates. When you actually sit down to write a class or function template, you can consult this chapter and Chapter 11 for a reference on the proper syntax.