Advanced Templates
Chapter 12, “Writing Generic Code with Templates,” covers 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 Standard Library works, or perhaps write your own simple class and function templates, 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 some obscure, but fascinating, details.
MORE ABOUT TEMPLATE PARAMETERS
Section titled “MORE ABOUT TEMPLATE PARAMETERS”There are three kinds of template parameters: template type parameters, non-type template parameters, and template template parameters. 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
Section titled “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 a container on which to build the grid. The Standard Library defines several parametrized container classes, including vector and deque. The original Grid class uses a vector to store the elements of a grid. A user of the Grid class might want to use a deque 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. The Grid implementation requires the underlying container to support random access. It also uses the resize() member function of the container and the container’s value_type type alias. A concept (see Chapter 12) is used to enforce that the provided container type supports these operations. Here is the concept and the class template definition with the additional template type parameter. Changes are highlighted.
template <typename Container>concept GridContainerType = std::ranges::random_access_range<Container> && requires(Container c) { typename Container::value_type; c.resize(1); };
export template <typename T, GridContainerType Container>class Grid{ public: explicit Grid(std::size_t width = DefaultWidth, std::size_t height = DefaultHeight); virtual ˜Grid() = default;
// Explicitly default a copy constructor and assignment operator. Grid(const Grid& src) = default; Grid& operator=(const Grid& rhs) = default;
// Explicitly default a move constructor and assignment operator. Grid(Grid&& src) = default; Grid& operator=(Grid&& rhs) = default;
typename Container::value_type& at(std::size_t x, std::size_t y); const typename Container::value_type& at( std::size_t x, std::size_t y) const;
std::size_t getHeight() const { return m_height; } std::size_t getWidth() const { return m_width; }
static constexpr std::size_t DefaultWidth { 10 }; static constexpr std::size_t DefaultHeight { 10 };
private: void verifyCoordinate(std::size_t x, std::size_t y) const;
Container m_cells; std::size_t m_width { 0 }, m_height { 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>.
The m_cells data member is now of type Container instead of vector<optional<T>>. Each Container type has a type alias called value_type. This is verified with the GridContainerType concept. Inside the Grid class template definition and its member function definitions, you get access to this value_type type name using the scope resolution operator: Container::value_type. However, since Container is a template type parameter, Container::value_type is a dependent type name. Usually, a compiler won’t treat dependent names as names of types, and this can lead to some rather cryptic compiler error messages. To make sure the compiler does interpret it as the name of a type, you need to prefix it with the typename keyword, as in typename Container::value_type. This is what is done for the return type of the at() member functions; their return type is the type of the elements that is stored inside the given container type, which is typename Container::value_type.
Here is the constructor definition:
template <typename T, GridContainerType Container>Grid<T, Container>::Grid(std::size_t width, std::size_t height) : m_width { width }, m_height { height }{ m_cells.resize(m_width * m_height);}Here are the implementations of the remaining member functions:
template <typename T, GridContainerType Container>void Grid<T, Container>::verifyCoordinate(std::size_t x, std::size_t y) const{ if (x >= m_width) { throw std::out_of_range { std::format("x ({}) must be less than width ({}).", x, m_width) }; } if (y>= m_height) { throw std::out_of_range { std::format("y ({}) must be less than height ({}).", y, m_height) }; }}
template <typename T, GridContainerType Container>const typename Container::value_type& Grid<T, Container>::at(std::size_t x, std::size_t y) const{ verifyCoordinate(x, y); return m_cells[x + y * m_width];}
template <typename T, GridContainerType Container>typename Container::value_type& Grid<T, Container>::at(std::size_t x, std::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;println("{}", myIntVectorGrid.at(3, 4).value_or(0));
myIntDequeGrid.at(1, 2) = 3;println("{}", myIntDequeGrid.at(1, 2).value_or(0));
Grid<int, vector<optional<int>>> grid2 { myIntVectorGrid };grid2 = myIntVectorGrid;You could try to instantiate the Grid class template with double for the Container template type parameter:
Grid<int, double> test; // WILL NOT COMPILEThis line does not compile. The compiler complains that the type double does not satisfy the constraints of the concept associated with the Container template type parameter.
Just as with function parameters, you can give template parameters default values. For example, you might want to say that the default container for a Grid is a vector. The class template definition then looks like this:
export template <typename T, GridContainerType 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 type parameter as the argument to the optional template in the default value for the second template type parameter. The C++ syntax requires that you do not repeat the default value in the template header line for member function definitions. With this default argument, clients can now instantiate a Grid and optionally specify an underlying container. Here are some examples:
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, priority_queue, flat_(multi)set, and flat_(multi)map class templates all take a Container template type parameter, with a default value, specifying the underlying container.
Introducing Template Template Parameters
Section titled “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 template 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 type 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 25, “Customizing and Extending the Standard Library.”
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) and replace the class name (vector) with your parameter name (Container). Given the preceding template specification, here is the class template definition for Grid that takes a container template as its second template parameter:
export 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(std::size_t x, std::size_t y); const std::optional<T>& at(std::size_t x, std::size_t y) const; // Omitted code that is the same as before. private: void verifyCoordinate(std::size_t x, std::size_t y) const;
Container<std::optional<T>> m_cells; std::size_t m_width { 0 }, m_height { 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. 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 m_cells is now as follows:
Container<std::optional<T>> m_cells;The member function definitions don’t need to change, except that you must change the template headers, for example:
template <typename T, template <typename E, typename Allocator = std::allocator<E>> class Container>void Grid<T, Container>::verifyCoordinate(std::size_t x, std::size_t y) const{ // Same implementation as before…}This Grid class template can be used as follows:
Grid<int, vector> myGrid;myGrid.at(1, 2) = 3;println("{}", myGrid.at(1, 2).value_or(0));Grid<int, vector> myGrid2 { myGrid };Grid<int, deque> myDequeGrid;This section demonstrated that you can pass templates as type parameters to other templates. However, the syntax looks a bit convoluted, and it is. I recommend avoiding template template parameters. In fact, the Standard Library itself never uses template template parameters.
More About Non-type Template Parameters
Section titled “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 the zero-initialization syntax, T{}, as the default value for the second template parameter.
export template <typename T, 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. You can use this initial value for T to initialize each cell in the grid:
template <typename T, T DEFAULT>Grid<T, DEFAULT>::Grid(std::size_t width, std::size_t height) : m_width { width }, m_height { height }{ m_cells.resize(m_width * m_height, DEFAULT);}The other member function definitions stay the same, except that you must add the second template parameter to the template headers, 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 int{}, i.e., 0Grid<int, 10> myIntGrid2; // Initial value is 10The initial value can be any integer you want. However, suppose that you try to create a Grid for SpreadsheetCells as follows:
SpreadsheetCell defaultCell;Grid<SpreadsheetCell, defaultCell> mySpreadsheet; // WILL NOT COMPILEThe second line leads to a compilation error because the value of the template parameter DEFAULT must be known at compile time; the value of defaultCell can’t be known until run time, so it is not an acceptable value for DEFAULT.
Up until C++20, non-type template parameters cannot be objects, or even doublesorfloats. They are restricted to integral types, enums, pointers, and references. Since C++20, these restrictions are relaxed a bit and it is now allowed to have non-type template parameters of floating-point types, and even certain class types. However, such class types have a lot of restrictions, not further discussed in this book. Suffice to say, the SpreadsheetCell class does not adhere to those restrictions.
CLASS TEMPLATE PARTIAL SPECIALIZATION
Section titled “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 template 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:
export template <typename T, std::size_t WIDTH, std::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;
// Explicitly default a move constructor and assignment operator. Grid(Grid&& src) = default; Grid& operator=(Grid&& rhs) = default;
std::optional<T>& at(std::size_t x, std::size_t y); const std::optional<T>& at(std::size_t x, std::size_t y) const;
std::size_t getHeight() const { return HEIGHT; } std::size_t getWidth() const { return WIDTH; } private: void verifyCoordinate(std::size_t x, std::size_t y) const;
std::optional<T> m_cells[WIDTH][HEIGHT];};You can specialize this class template for const char* C-style strings like this:
export template <std::size_t WIDTH, std::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;
// Explicitly default a move constructor and assignment operator. Grid(Grid&& src) = default; Grid& operator=(Grid&& rhs) = default;
std::optional<std::string>& at(std::size_t x, std::size_t y); const std::optional<std::string>& at(std::size_t x, std::size_t y) const;
std::size_t getHeight() const { return HEIGHT; } std::size_t getWidth() const { return WIDTH; } private: void verifyCoordinate(std::size_t x, std::size_t y) const;
std::optional<std::string> m_cells[WIDTH][HEIGHT];};In this case, you are not specializing all the template parameters. Therefore, your template header looks like this:
export template <std::size_t WIDTH, std::size_t HEIGHT>class Grid<const char*, WIDTH, HEIGHT>This class 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 GridGrid<const char*, 2, 2> myStringGrid; // Uses the partial specializationGrid<2, 3> test; // DOES NOT COMPILE! No type specified.Yes, the syntax might be confusing. Additionally, in partial specializations, unlike in full specializations, you must include the template header in front of every member function definition, as in the following example:
template <std::size_t WIDTH, std::size_t HEIGHT>const std::optional<std::string>& Grid<const char*, WIDTH, HEIGHT>::at(std::size_t x, std::size_t y) const{ verifyCoordinate(x, y); return m_cells[x][y];}You need this template header with two parameters to show that this member function 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 perform deep copies of objects to which pointers point, instead of shallow copies.
The following is the class definition, assuming that you’re specializing the initial version of Grid with only one template parameter. In this implementation, Grid becomes the owner of supplied data, so it automatically frees the memory when necessary. Copy/move constructors and copy/move assignment operators are required. As usual, the copy assignment operator uses the copy-and-swap idiom, and the move assignment operator uses the move-and-swap idiom, as discussed in Chapter 9, “Mastering Classes and Objects,” which requires a noexcept swap() member function.
export template <typename U>class Grid<U*>{ public: explicit Grid(std::size_t width = DefaultWidth, std::size_t height = DefaultHeight); virtual ˜Grid() = default;
// Copy constructor and copy assignment operator. Grid(const Grid& src); Grid& operator=(const Grid& rhs);
// Move constructor and move assignment operator. Grid(Grid&& src) noexcept; Grid& operator=(Grid&& rhs) noexcept;
void swap(Grid& other) noexcept;
std::unique_ptr<U>& at(std::size_t x, std::size_t y); const std::unique_ptr<U>& at(std::size_t x, std::size_t y) const;
std::size_t getHeight() const { return m_height; } std::size_t getWidth() const { return m_width; }
static constexpr std::size_t DefaultWidth { 10 }; static constexpr std::size_t DefaultHeight { 10 };
private: void verifyCoordinate(std::size_t x, std::size_t y) const;
std::vector<std::unique_ptr<U>> m_cells; std::size_t m_width { 0 }, m_height { 0 };};As usual, these two lines are the crux of the matter:
export template <typename U>class Grid<U*>This syntax states that this class template is a specialization of the Grid class template for all pointer types. Important to know, when you have an instantiation such as Grid<int*>, then U is int, not int*. That might be a bit unintuitive, but 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 != nullptr) { println("{}", *element); *element = 6;}println("{}", *psGrid.at(1, 0)); // psGrid is not modified.println("{}", *psGrid2.at(1, 0)); // psGrid2 is modified.Here is the output:
336The implementations of the member functions 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 U>Grid<U*>::Grid(const Grid& src) : Grid { src.m_width, src.m_height }{ // 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 (std::size_t i { 0 }; i < m_cells.size(); ++i) { // Make a deep copy of the element by using its copy constructor. if (src.m_cells[i] != nullptr) { m_cells[i] = std::make_unique<U>(*src.m_cells[i]); } }}EMULATING FUNCTION PARTIAL SPECIALIZATION WITH OVERLOADING
Section titled “EMULATING FUNCTION PARTIAL SPECIALIZATION WITH OVERLOADING”The C++ standard does not permit partial template specialization of function templates. Instead, you can overload the function template with another function template. As an example, let’s look again at the Find() algorithm from Chapter 12. It consists of a generic Find() function template and a non-template overload for const char*s. Here is a reminder:
template <typename T>optional<size_t> Find(const T& value, const 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 {}; // failed to find it; return empty optional.}
optional<size_t> Find(const char* value, const char** arr, size_t size){ for (size_t i { 0 }; i < size; ++i) { if (strcmp(arr[i], value) == 0) { return i; // found it; return the index. } } return {}; // failed to find it; return empty optional.}Suppose that you want to customize Find() so that it dereferences pointers to use operator== directly on the objects pointed to. The correct way to implement this behavior is to overload the Find() function template with another, more specialized, function template:
template <typename T>optional<size_t> Find(T* 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 {}; // failed to find it; return empty optional.}The following code calls Find() several times. The comments say which overload of Find() is called.
optional<size_t> res;
int myInt { 3 }, intArray[] { 1, 2, 3, 4 };size_t sizeArray { size(intArray) };res = Find(myInt, intArray, sizeArray); // calls Find<int> by deductionres = Find<int>(myInt, intArray, sizeArray); // calls Find<int> explicitly
double myDouble { 5.6 }, doubleArray[] { 1.2, 3.4, 5.7, 7.5 };sizeArray = size(doubleArray);// calls Find<double> by deductionres = Find(myDouble, doubleArray, sizeArray);// calls Find<double> explicitlyres = Find<double>(myDouble, doubleArray, sizeArray);
const char* word { "two" };const char* words[] { "one", "two", "three", "four" };sizeArray = size(words);// calls Find<const char*> explicitlyres = Find<const char*>(word, words, sizeArray);// calls overloaded Find for const char*sres = Find(word, words, sizeArray);
int *intPointer { &myInt }, *pointerArray[] { &myInt, &myInt };sizeArray = size(pointerArray);// calls the overloaded Find for pointersres = Find(intPointer, pointerArray, sizeArray);
SpreadsheetCell cell1 { 10 };SpreadsheetCell cellArray[] { SpreadsheetCell { 4 }, SpreadsheetCell { 10 } };sizeArray = size(cellArray);// calls Find<SpreadsheetCell> by deductionres = Find(cell1, cellArray, sizeArray);// calls Find<SpreadsheetCell> explicitlyres = Find<SpreadsheetCell>(cell1, cellArray, sizeArray);
SpreadsheetCell *cellPointer { &cell1 };SpreadsheetCell *cellPointerArray[] { &cell1, &cell1 };sizeArray = size(cellPointerArray);// Calls the overloaded Find for pointersres = Find(cellPointer, cellPointerArray, sizeArray);TEMPLATE RECURSION
Section titled “TEMPLATE RECURSION”Templates in C++ provide capabilities that go far beyond the simple class and function templates you have seen so far in this chapter and Chapter 12. One of these capabilities is template recursion. Template recursion is similar to function recursion, in which a function is defined in terms of calling itself with a slightly easier version of the problem. This section first provides a motivation for template recursion and then shows how to implement it.
An N-Dimensional Grid: First Attempt
Section titled “An N-Dimensional Grid: First Attempt”Up to now, the Grid class template supports only two dimensions, which limits its usefulness. What if you want 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 a OneDGrid class template. It’s simply a one-dimensional version of the Grid class template from earlier examples, with the addition of a resize() member function, 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. For this example, m_elements stores instances of T instead of instances of std::optional<T>.
export template <typename T>class OneDGrid final{ public: explicit OneDGrid(std::size_t size = DefaultSize) { resize(size); }
T& operator[](std::size_t x) { return m_elements[x]; } const T& operator[](std::size_t x) const { return m_elements[x]; }
void resize(std::size_t newSize) { m_elements.resize(newSize); } std::size_t getSize() const { return m_elements.size(); }
static constexpr std::size_t DefaultSize { 10 }; private: std::vector<T> m_elements;};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 the next section explains, we can do better.
A Real N-Dimensional Grid
Section titled “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 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.
The following shows the NDGrid class template definition and implementation, with highlights showing where it differs from the OneDGrid shown in the previous section. The m_elements data member is now 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 trickiest aspect of the implementation, 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. A user should have the ability to create a grid with a specified size, such as 20 or 50. Thus, the constructor takes an integer size parameter. The resize() member function is modified to resize m_elements and to initialize each element with NDGrid<T, N-1> { newSize }, which recursively resizes all dimensions of the grid to the new size.
export template <typename T, std::size_t N>class NDGrid final{ public: explicit NDGrid(std::size_t size = DefaultSize) { resize(size); }
NDGrid<T, N-1>& operator[](std::size_t x) { return m_elements[x]; } const NDGrid<T, N-1>& operator[](std::size_t x) const { return m_elements[x]; }
void resize(std::size_t newSize) { m_elements.resize(newSize, NDGrid<T, N-1> { newSize }); }
std::size_t getSize() const { return m_elements.size(); }
static constexpr std::size_t DefaultSize { 10 }; private: std::vector<NDGrid<T, N-1>> m_elements;};The template definition for the base case is a partial specialization for dimension 1. The following shows the definition and implementation. You must rewrite a lot of the code because a specialization never inherits any code from the primary template. Highlights show the differences from the non-specialized NDGrid.
export template <typename T>class NDGrid<T, 1> final{ public: explicit NDGrid(std::size_t size = DefaultSize) { resize(size); }
T& operator[](std::size_t x) { return m_elements[x]; } const T& operator[](std::size_t x) const { return m_elements[x]; }
void resize(std::size_t newSize) { m_elements.resize(newSize); } std::size_t getSize() const { return m_elements.size(); }
static constexpr std::size_t DefaultSize { 10 }; private: std::vector<T> m_elements;};Here the recursion ends: the element type is T, not another template instantiation.
Now, you can write code like this:
NDGrid<int, 3> my3DGrid { 4 };my3DGrid[2][1][2] = 5;my3DGrid[1][1][1] = 5;println("{}", my3DGrid[2][1][2]);To avoid the code duplication between the primary template and the specialization, you could pull the duplicate code out into a base class and then derive both the primary template and the specialization from that base class; but in this small example, the overhead added by that technique would outweigh the savings.
VARIADIC TEMPLATES
Section titled “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 template arguments, as in this example:
MyVariadicTemplate<int> instance1;MyVariadicTemplate<string, double, vector<int>> instance2;It can even be instantiated with zero template arguments:
MyVariadicTemplate<> instance3;To disallow instantiating a variadic template with zero template arguments, you can write the template as follows:
template <typename T1, typename… Types>class MyVariadicTemplate { };With this definition, trying to instantiate MyVariadicTemplate with zero template arguments results in a compilation error.
It is not possible to directly iterate over the arguments given to a variadic template. The only way you can do this is with the aid of template recursion or fold expressions. The following sections show examples of both.
Type-Safe Variable-Length Argument Lists
Section titled “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 you have to write an overload of handleValue() for each type that you want to handle—int, double, and string in this example:
void handleValue(int value) { println("Integer: {}", value); }void handleValue(double value) { println("Double: {}", value); }void handleValue(const string& value) { println("String: {}", value); }
void processValues() // Base case to stop recursion{ /* Nothing to do in this base case. */ }
template <typename T1, typename… Tn>void processValues(const T1& arg1, const Tn&… args){ handleValue(arg1); processValues(args…);}This example demonstrates a 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 statement:
processValues(args…);This statement 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 parameter, T1. The act of recursively calling processValues() with args… is that on each call there is one 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 implementation of variable-length argument lists is fully type-safe. The processValues() function automatically calls the correct handleValue() overload based on the actual type. The compiler will issue an error when you call processValues() with an argument of a certain type for which there is no handleValue() overload defined.
You can also use forwarding references, introduced in Chapter 12, in the implementation of processValues(). 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 is passed, it is forwarded as an lvalue reference.
void processValues() // Base case to stop recursion{ /* Nothing to do in this base case.*/ }
template <typename T1, typename… Tn>void processValues(T1&& arg1, Tn&&… args){ handleValue(forward<T1>(arg1)); processValues(forward<Tn>(args)…);}There is one statement that needs further explanation:
processValues(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(forward<A1>(a1), forward<A2>(a2), forward<A3>(a3));Inside the body of a function using a parameter pack, you can retrieve the number of arguments in the pack using sizeof…(pack). Notice that this is not doing a pack expansion with …, but is using the special keyword-like syntax sizeof…
int numberOfArguments { 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.
constexpr if
Section titled “constexpr if”constexpr if statements 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. Such compile-time decisions can come in handy with variadic templates. For example, the earlier implementation of processValues() requires a base case to stop the recursion (void processValues(){}). Using constexpr if, such a base case can be avoided. Notice that the feature is officially called constexpr if, but in actual code you write if constexpr.
template <typename T1, typename… Tn>void processValues(T1&& arg1, Tn&&… args){ handleValue(forward<T1>(arg1)); if constexpr (sizeof…(args) > 0) { processValues(forward<Tn>(args)…); }}In this implementation, the recursion stops as soon as the variadic parameter pack, args, becomes empty. The only difference with the previous implementations is that you can no longer call processValues() without any arguments. Doing so results in a compilation error.
Variable Number of Mixin Classes
Section titled “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, “Designing with Classes,” discusses the concept of mixin classes.
class Mixin1{ public: explicit Mixin1(int i) : m_value { i } {} virtual void mixin1Func() { println("Mixin1: {}", m_value); } private: int m_value;};
class Mixin2{ public: explicit Mixin2(int i) : m_value { i } {} virtual void mixin2Func() { println("Mixin2: {}", m_value); } private: int m_value;};
template <typename… Mixins>class MyClass : public Mixins…{ public: explicit 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 a 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: 11Mixin2: 22Mixin1: 33Fold Expressions
Section titled “Fold Expressions”C++ supports fold expressions. This makes working with parameter packs in variadic templates much easier. Fold expressions can be used to apply a certain operation to every value of a parameter pack, to reduce all values in a parameter pack to a single value, and more.
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(forward<T1>(arg1)); processValues(forward<Tn>(args)…);}Because it is defined recursively, it needs a base case to stop the recursion. With fold expressions, this can be implemented with a single function template using a unary right fold over the comma operator:
template <typename… Tn>void processValues(Tn&&… args) { (handleValue(forward<Tn>(args)), …); }Basically, the three dots in the function body trigger folding with the comma operator for Ѳ. 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, of three types, A1, A2, and A3. The expansion of the unary right fold then becomes as follows:
(handleValue(forward<A1>(a1)), (handleValue(forward<A2>(a2)) , handleValue(forward<A3>(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) { (println("{}", values), …); }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:
(println("{}", v1), (println("{}", v2), println("{}", v3)));You can call printValues() with as many arguments as you want, for example:
printValues(1, "test", 2.34);In the examples up to now, the folding is done 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). Hence, sumValues() has two template type parameters: a normal one to specify the type of Init, and a parameter pack that can accept 0 or more arguments.
template <typename T, typename… Values>auto 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 tested as follows:
println("{}", sumValues(1, 2, 3.3));println("{}", sumValues(1));The sumValues() function template can also be defined in terms of a unary left fold as follows.
template <typename… Values>auto sumValues(const Values&… values) { return (… + values); }Concepts, discussed in Chapter 12, can also be variadic. For example, the sumValues() function template can be constrained so that it can be called only with a set of arguments of the same type:
template <typename T, typename… Us>concept SameTypes = (std::same_as<T, Us> && …);
template <typename T, typename… Values> requires SameTypes<T, Values…>auto sumValues(const T& init, const Values&… values){ return (init + … + values); }Calling this constrained version as follows works fine:
println("{}", sumValues(1.1, 2.2, 3.3)); // OK: 3 doubles, output is 6.6println("{}", sumValues(1)); // OK: 1 integer, output is 1println("{}", sumValues("a"s, "b"s)); // OK: 2 strings, output is abHowever, the following call fails as the argument list contains an integer and two doubles:
println("{}", sumValues(1, 2.2, 3.3)); // ErrorParameter packs with zero length are allowed for unary folds, but only in combination with the logical AND (&&), logical OR (||), and comma (,) operators. For an empty parameter pack, applying && to it results in true, applying || results in false, and applying , results in void(), i.e., a no-op. For example:
template <typename… Values>bool allEven(const Values&… values) { return ((values % 2 == 0) && …); }
template <typename… Values>bool anyEven(const Values&… values) { return ((values % 2 == 0) || …); }
int main(){ println("{} {} {}", allEven(2,4,6), allEven(2,3), allEven());//true false true println("{} {} {}", anyEven(1,2,3), anyEven(1,3), anyEven());//true false false}METAPROGRAMMING
Section titled “METAPROGRAMMING”This section touches on template metaprogramming. It is a complicated and broad 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 another programming language. 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
Section titled “Factorial at Compile Time”Template metaprogramming allows you to perform calculations at compile time instead of at run time. The following code is an 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 <int f>class Factorial{ public: static constexpr unsigned long long value { f * Factorial<f - 1>::value };};
template <>class Factorial<0>{ public: static constexpr unsigned long long value { 1 };};
int main(){ println("{}", Factorial<6>::value);}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 need to use template metaprogramming. You can implement it as a consteval immediate function as follows, without any templates, though the template implementation still serves as a good example on how to implement recursive templates.
consteval unsigned long long factorial(int f){ if (f == 0) { return 1; } else { return f * factorial(f - 1); }}You can call factorial() just as you would call any other function, with the difference that the consteval function is guaranteed to be executed at compile time. For example:
println("{}", factorial(6));Loop Unrolling
Section titled “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 be done only when you really need it, for example in performance critical code. 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 class template instantiates itself with i-1. When it hits 0, the recursion stops.
template <int i>class Loop{ public: template <typename FuncType> static void run(FuncType func) { Loop<i - 1>::run(func); func(i); }};
template <>class Loop<0>{ public: template <typename FuncType> static void run(FuncType /* func */) { }};The Loop template can be used as follows:
void doWork(int i) { println("doWork({})", i); }
int main(){ Loop<3>::run(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)Printing Tuples
Section titled “Printing Tuples”This example uses template metaprogramming to print the individual elements of an std::tuple. Tuples are explained in Chapter 24, “Additional Vocabulary Types.” 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 can 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 TuplePrint 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 TuplePrint stops the recursion when this integer hits 0. The main() function shows how this TuplePrint class template can be used.
template <typename TupleType, int N>class TuplePrint{ public: explicit TuplePrint(const TupleType& t) { TuplePrint<TupleType, N − 1> tp { t }; println("{}", get<N − 1>(t)); }};
template <typename TupleType>class TuplePrint<TupleType, 0>{ public: explicit TuplePrint(const TupleType&) { }};
int main(){ using MyTuple = tuple<int, string, bool>; MyTuple t1 { 16, "Test", true }; TuplePrint<MyTuple, tuple_size<MyTuple>::value> tp { t1 };}The TuplePrint statement in main() looks a bit complicated because it requires the exact type and size of the tuple as template arguments. This can be simplified by introducing a helper function template that automatically deduces the template parameters. The simplified implementation is as follows:
template <typename TupleType, int N>class TuplePrintHelper{ public: explicit TuplePrintHelper(const TupleType& t) { TuplePrintHelper<TupleType, N − 1> tp { t }; println("{}", get<N − 1>(t)); }};
template <typename TupleType>class TuplePrintHelper<TupleType, 0>{ public: explicit TuplePrintHelper(const TupleType&) { }};
template <typename T>void tuplePrint(const T& t){ TuplePrintHelper<T, tuple_size<T>::value> tph { t };}
int main(){ tuple t1 { 16, "Test"s, true }; tuplePrint(t1);}The first change made here is renaming the original TuplePrint class template to TuplePrintHelper. The code then implements a small function template called tuplePrint(). 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 TuplePrintHelper class template. The main() function shows how to use this simplified version. You don’t need to specify the function template parameter because the compiler can deduce this automatically from the supplied argument.
constexpr if
Section titled “constexpr if”constexpr if, introduced earlier in this chapter, can be used to simplify a lot of template metaprogramming techniques. For example, you can simplify the previous code for printing elements of a tuple using constexpr if, as follows. 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 TuplePrintHelper{ public: explicit TuplePrintHelper(const TupleType& t) { if constexpr (N > 1) { TuplePrintHelper<TupleType, N − 1> tp { t }; } println("{}", get<N − 1>(t)); }};
template <typename T>void tuplePrint(const T& t){ TuplePrintHelper<T, tuple_size<T>::value> tph { t };}Now we can even get rid of the class template itself and replace it with a simple function template called tuplePrintHelper():
template <typename TupleType, int N>void tuplePrintHelper(const TupleType& t){ if constexpr (N > 1) { tuplePrintHelper<TupleType, N − 1>(t); } println("{}", get<N − 1>(t));}
template <typename T>void tuplePrint(const T& t){ tuplePrintHelper<T, tuple_size<T>::value>(t);}This can be simplified even more. Both function templates can be combined into one, as follows:
template <typename TupleType, int N = tuple_size<TupleType>::value>void tuplePrint(const TupleType& t){ if constexpr (N > 1) { tuplePrint<TupleType, N − 1>(t); } println("{}", get<N − 1>(t));}It can still be called the same as before:
tuple t1 { 16, "Test"s, true };tuplePrint(t1);Using a Compile-Time Integer Sequence with Folding
Section titled “Using a Compile-Time Integer Sequence with Folding”C++ supports compile-time integer sequences using std::integer_sequence, 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::make_index_sequence to generate an index sequence of the same length as the length of a given parameter pack.
The tuple printer can be implemented using variadic templates, compile-time index sequences, and fold expressions as follows:
template <typename Tuple, size_t… Indices>void tuplePrintHelper(const Tuple& t, index_sequence<Indices…>){ (println("{}", get<Indices>(t)) , …);}
template <typename… Args>void tuplePrint(const tuple<Args…>& t){ tuplePrintHelper(t, make_index_sequence<sizeof…(Args)>{});}It can be called in the same way as before:
tuple t1 { 16, "Test"s, true };tuplePrint(t1);With this call, the unary right fold expression in the tuplePrintHelper() function template expands to the following:
((println("{}", get<0>(t)), (println("{}", get<1>(t)), println("{}", get<2>(t)))));Type Traits
Section titled “Type Traits”Type traits allow you to make decisions based on types at compile time. For example, you can verify that a type is derived from another type, is convertible to another type, is integral, and so on. The C++ Standard Library comes with a large selection of type traits. All type traits-related functionality is defined in <type_traits>. Type traits are divided into separate categories. The following list gives a couple of examples of the available type traits in each category. Consult a Standard Library reference (see Appendix B, “Annotated Bibliography”) for a complete list.
- Primary type categories - is_void - is_integral - is_floating_point - is_pointer - is_function - … - Type properties - is_const - 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 - is_scoped_enum* - is_implicit_lifetime* - … - Property queries - alignment_of - rank - extent | - Composite type categories - is_arithmetic - is_reference - is_object - is_scalar - … - Type relationships - is_same - is_base_of - is_convertible - is_invocable - is_nothrow_invocable - span Start cssStyle="font-family:monospace"?… - const-volatile modifications - remove_const - add_const - span Start cssStyle="font-family:monospace"?… - Sign modifications - make_signed - make_unsigned - Array modifications - remove_extent - remove_all_extents - Logical operator traits - conjunction - disjunction - negation |
- Reference modifications - remove_reference - add_lvalue_reference - add_rvalue_reference - Pointer modifications - remove_pointer - add_pointer - Constant evaluation context - is_constant_evaluated | - Other transformations - enable_if - conditional - invoke_result - type_identity - remove_cvref - common_reference - decay - … |
The type traits marked with an asterisk (*) are available only since C++23.
Type traits are a rather 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
Section titled “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>;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, which checks whether a type is an integral type, and is_class, which checks whether a type is a class, inherit from either true_type or false_type. For example, the Standard Library specializes is_integral 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) { println("int is integral"); }else { println("int is not integral"); }
if (is_class<string>::value) { println("string is a class"); }else { println("string is not a class"); }The output is as follows:
int is integralstring is a classFor each trait that has a value member, the Standard Library 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 an example of how the is_integral_v<T> variable template is defined in the Standard Library:
template <class T>inline constexpr bool is_integral_v = is_integral<T>::value;Using these variable templates, the previous example can be written as follows:
if (is_integral_v<int>) { println("int is integral"); }else { println("int is not integral"); }
if (is_class_v<string>) { println("string is a class"); }else { println("string is not a class"); }In fact, because the value of is_integral_v<T> is a compile-time constant, you could use a constexpr if instead of a normal if.
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 function templates demonstrate this. The code defines two overloaded processHelper() 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 processHelper().
template <typename T>void processHelper(const T& t, true_type){ println("{} is an integral type.", t);}
template <typename T>void processHelper(const T& t, false_type){ println("{} is a non-integral type.", t);}
template <typename T>void process(const T& t){ processHelper(t, is_integral<T>{});}The second argument in the call to processHelper() is is_integral<T>{}. This argument uses is_integral<T> to figure out if T is an integral type. is_integral<T> derives from either true_type or false_type. The processHelper() function needs an instance of a true_type or a false_type as a second parameter, so that is the reason for the empty set of braces {}. The two overloaded processHelper() functions don’t bother to name the 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 used only 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 can 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>) { println("{} is an integral type.", t); } else { println("{} is a non-integral type.", t); }}Using Type Relationships
Section titled “Using Type Relationships”Some examples of type relationships are is_same, is_base_of, and is_convertible. This section gives an example of how to use is_same; the other type relationships work similarly.
The following same() function template uses the is_same type trait to figure out whether 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> }; println("'{}' and '{}' are {} types.", t1, t2, (areTypesTheSame ? "the same" : "different"));}
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 typesAlternatively, you can implement this example without using any type traits, but using an overload set of two function templates instead:
template <typename T1, typename T2>void same(const T1& t1, const T2& t2){ println("'{}' and '{}' are different types.", t1, t2);}template <typename T>void same(const T& t1, const T& t2){ println("'{}' and '{}' are the same type.", t1, t2);}The second function template is more specialized than the first, so it will be preferred by overload resolution whenever it is viable, that is, whenever both arguments are of the same type T.
Using the conditional Type Trait
Section titled “Using the conditional Type Trait”Chapter 18, “Standard Library Containers,” explains the Standard Library helper function template std::move_if_noexcept(), which can be used to conditionally call either the move constructor or the copy constructor depending on whether the former is marked noexcept. The Standard Library does not provide a similar helper function template to easily call the move assignment operator or copy assignment operator depending on whether the former is noexcept. Now that you know about template metaprogramming and type traits, let’s take a look at how to implement a move_assign_if_noexcept() ourselves.
Remember from Chapter 18 that move_if_noexcept() just converts a given reference to an rvalue reference if the move constructor is marked noexcept and to a reference-to-const otherwise.
move_assign_if_noexcept() needs to do something similar, convert a given reference to an rvalue reference if the move assignment operator is marked noexcept, and to a reference-to-const otherwise.
The std::conditional type trait can be used to implement the condition. This type trait has three template parameters: a Boolean, a type for when the Boolean is true, and a type for when it is false. The implementation of the conditional type trait looks as follows:
template <bool B, class T, class F>struct conditional { using type = T; };
template <class T, class F>struct conditional<false, T, F> { using type = F; };The is_nothrow_move_assignable type trait can be used to figure out whether a certain type can be move assigned without throwing exceptions. For class types, this means to check if the type has a move assignment operator that is marked with noexcept. Here is the entire implementation of move_assign_if_noexcept():
template <typename T>constexpr conditional<is_nothrow_move_assignable_v<T>, T&&, const T&>::type move_assign_if_noexcept(T& t) noexcept{ return move(t);}The Standard Library defines alias templates for traits that have a type member, such as conditional. These have the same name as the trait, but are appended with _t. For example, the conditional_t<B,T,F> alias template for conditional<B,T,F>::type is defined by the Standard Library as follows:
template <bool B, class T, class F>using conditional_t = typename conditional<B,T,F>::type;So, instead of writing this:
conditional<is_nothrow_move_assignable_v<T>, T&&, const T&>::typeyou can write this:
conditional_t<is_nothrow_move_assignable_v<T>, T&&, const T&>The move_assign_if_noexcept() function template can be tested as follows:
class MoveAssignable{ public: MoveAssignable& operator=(const MoveAssignable&) { println("copy assign"); return *this; } MoveAssignable& operator=(MoveAssignable&&) { println("move assign"); return *this; }};
class MoveAssignableNoexcept{ public: MoveAssignableNoexcept& operator=(const MoveAssignableNoexcept&) { println("copy assign"); return *this; } MoveAssignableNoexcept& operator=(MoveAssignableNoexcept&&) noexcept { println("move assign"); return *this; }};
int main(){ MoveAssignable a, b; a = move_assign_if_noexcept(b); MoveAssignableNoexcept c, d; c = move_assign_if_noexcept(d);}This outputs the following: The output is as follows:
copy assignmove assignUsing Type Modification Type Traits
Section titled “Using Type Modification Type Traits”A number of type traits modify a given type. For example, the add_const type trait adds const to a given type, the remove_pointer type trait removes the pointer from a type, and so on. Here’s an example:
println("{}", is_same_v<string, remove_pointer_t<string*>>);The output is true.
Implementing such type modification traits yourself is not that hard. Here is an implementation of a my_remove_pointer type trait (slightly simplified):
// my_remove_pointer class template.template <typename T> struct my_remove_pointer { using type = T; };// Partial specialization for pointer types.template <typename T> struct my_remove_pointer<T*> { using type = T; };// Partial specialization for const pointer types.template <typename T> struct my_remove_pointer<T* const> { using type = T; };// Alias template for ease of use.template <typename T>using my_remove_pointer_t = typename my_remove_pointer<T>::type;
int main(){ println("{}", is_same_v<string, my_remove_pointer_t<string*>>);}Using enable_if
Section titled “Using enable_if”The use of enable_if is based on a principle called substitution failure is not an error (SFINAE), an advanced feature of C++. That principle states that a failure to specialize a function template for a given set of template parameters should not be seen as a compilation error. Instead, such specializations should just be removed from the function overload set. This section explains only 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 often used on the return types of your set of overloads, or with unnamed non-type template parameters. enable_if accepts two template parameters. The first is a Boolean, and the second is a type. If the Boolean is true, then the enable_if class template has a type alias that you can access using ::type. The type of this type alias is the type given as the second template parameter. If the Boolean is false, then there is no such type alias. Here is the implementation of this type trait:
template <bool B, class T = void>struct enable_if {};
template <class T>struct enable_if<true, T> { typedef T type; };The same() function template from an earlier section can be rewritten into overloaded checkType() function templates by using enable_if as follows. In this implementation, the checkType() functions return true or false depending on whether the types of the given values are the same. If you don’t want to return anything from checkType(), you can remove the return statements and remove the second template argument for enable_if.
template <typename T1, typename T2>enable_if_t<is_same_v<T1, T2>, bool> checkType(const T1& t1, const T2& t2){ println("'{}' and '{}' are the same types.", t1, t2); return true;}
template <typename T1, typename T2>enable_if_t<!is_same_v<T1, T2>, bool> checkType(const T1& t1, const T2& t2){ println("'{}' and '{}' are different types.", t1, t2); return false;}
int main(){ checkType(1, 32); checkType(1, 3.01); checkType(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 overloads for checkType(). It uses is_same_v to check whether 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 statement in main(), it tries to find a function checkType() that accepts two integer values. It finds the first checkType() function template overload 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 thus the compiler adds this overload to the set of candidates. When it sees the second overload of checkType(), it again deduces that it can use an instance of this function template by making T1 and T2 both integers. However, when trying to figure out the return type, it finds out that !is_same_v<T1, T2> is false. Because of this, enable_if_t<false, bool> does not represent a type, leaving that overload of checkType() without a return type. The compiler notices this error but does not yet generate a real compilation error because of SFINAE. It simply does not add this overload to the set of candidates. Thus, with the first statement in main(), the overload set contains one candidate checkType() function, so it’s clear which one the compiler will use.
When the compiler tries to compile the second statement in main(), it again tries to find a suitable checkType() function. It starts with the first checkType() 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 checkType() without a return type. The compiler notices this error but does not yet generate a real compilation error because of SFINAE. Instead, the compiler simply does not add this overload to the set of candidates. When the compiler sees the second checkType() function, it figures out that that one works out fine because T1 and T2 are of different types, so !is_same_v<T1, T2> is true, and thus enable_if_t<true, bool> is type bool. In the end, the overload set for the second statement in main() again contains only one overload, so it’s clear which one the compiler will use.
If you don’t want to clutter your return types with enable_if, then another option is to use enable_if with extra non-type template parameters. This actually makes the code easier to read. For example:
template <typename T1, typename T2, enable_if_t<is_same_v<T1, T2>>* = nullptr>bool checkType(const T1& t1, const T2& t2){ println("'{}' and '{}' are the same types.", t1, t2); return true;}
template <typename T1, typename T2, enable_if_t<!is_same_v<T1, T2>>* = nullptr>bool checkType(const T1& t1, const T2& t2){ println("'{}' and '{}' are different types.", t1, t2); return false;}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 must use it with non-type template parameters, as shown earlier.
The enable_if syntax explained in this section was the state of the art prior to C++20. Since C++20, you should prefer to use concepts, discussed in Chapter 12. Notice the syntactic similarity between the earlier enable_if code and the following example using concepts. However, it’s clear that the version using concepts is more readable.
template <typename T1, typename T2> requires is_same_v<T1, T2>bool checkType(const T1& t1, const T2& t2){ println("'{}' and '{}' are the same types.", t1, t2); return true;}
template <typename T1, typename T2> requires !is_same_v<T1, T2>bool checkType(const T1& t1, const T2& t2){ println("'{}' and '{}' are different types.", t1, t2); return false;}It is recommended to use SFINAE judiciously. Use it only when you need to resolve overload ambiguities that you cannot possibly resolve using any other technique, such as specializations, concepts, and so on. For example, if you just want compilation to fail when you use a template with the wrong types, use concepts or use static_assert(), explained later in this chapter, instead of SFINAE. Of course, there are legitimate use cases for SFINAE, but keep the following in mind.
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.
Using constexpr if to Simplify enable_if Constructs
Section titled “Using constexpr if to Simplify enable_if Constructs”As you can see from earlier examples, using enable_if can become quite complicated. The constexpr if feature helps to dramatically simplify certain use cases of enable_if.
For example, suppose you have the following two classes:
class IsDoable{ public: virtual void doit() const { println("IsDoable::doit()"); }};
class Derived : public IsDoable { };You can write a function template, callDoit(), that calls the doit() member function if the member function is available; otherwise, prints an error message. 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> callDoit(const T& t){ t.doit();}
template <typename T>enable_if_t<!is_base_of_v<IsDoable, T>, void> callDoit(const T&){ println("Cannot call doit()!");}The following code tests this implementation:
Derived d;callDoit(d);callDoit(123);Here is the output:
IsDoable::doit()Cannot call doit()!You can simplify this enable_if implementation a lot by using constexpr if:
template <typename T>void callDoit(const T& t){ if constexpr (is_base_of_v<IsDoable, T>) { t.doit(); } else { println("Cannot call doit()!"); }}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 statement 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 statement t.doit() won’t even be compiled.
Instead of using the is_base_of type trait, you can also use a requires expression; see Chapter 12. Here is an implementation of callDoit() using a requires expression to check whether the doit() member function can be called on object t.
template <typename T>void callDoit(const T& t){ if constexpr (requires { t.doit(); }) { t.doit(); } else { println("Cannot call doit()!"); }}Logical Operator Traits
Section titled “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 arguments and can be used to perform logical operations on type traits, as in this example:
print("{} ", conjunction_v<is_integral<int>, is_integral<short>>);print("{} ", conjunction_v<is_integral<int>, is_integral<double>>);
print("{} ", disjunction_v<is_integral<int>, is_integral<double>, is_integral<short>>);
print("{} ", negation_v<is_integral<int>>);The output is as follows:
true false true falseStatic Assertions
Section titled “Static Assertions”static_assert() allows certain conditions to be asserted at compile time. An assertion is something that needs to be true. If an assertion is false, the compiler issues an error. A call to static_assert() accepts two parameters: an expression to evaluate at compile time and (optionally) a string. When the expression evaluates to false, the compiler issues an error that contains the given string. An example is to check that you are compiling with a 64-bit compiler:
static_assert(sizeof(void*) == 8, "Requires 64-bit compilation.");If you compile this with a 32-bit compiler where a pointer is four bytes, the compiler issues an error that can look like this:
test.cpp(3): error C2338: Requires 64-bit compilation.The string parameter is optional, as in this example:
static_assert(sizeof(void*) == 8);In this case, if the expression evaluates to false, you get a compiler-dependent error message. For example, Microsoft Visual C++ gives the following error:
test.cpp(3): error C2607: static assertion failedstatic_assert() can be combined with type traits. Here is an example:
template <typename T>void foo(const T& t){ static_assert(is_integral_v<T>, "T must be an integral type.");}Metaprogramming Conclusion
Section titled “Metaprogramming Conclusion”As you have seen in this section, template metaprogramming can be a 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 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
Section titled “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. Ideally you have gained an appreciation for the power and capabilities of these features and an idea of how you can apply these techniques 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 techniques 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.
EXERCISES
Section titled “EXERCISES”By solving the following exercises, you can practice the material discussed in this chapter. Solutions to all exercises are available with the code download on the book’s website at www.wiley.com/go/proc++6e. However, if you are stuck on an exercise, first reread parts of this chapter to try to find an answer yourself before looking at the solution from the website.
-
Exercise 26-1: In Exercise 12-2, you wrote a full specialization of a
KeyValuePairclass template forconst char*keys and values. Replace that full specialization with a partial specialization where the values are of typeconst char*but the keys can be of any type. -
Exercise 26-2: Calculate the nth number in the Fibonacci series at compile time using template recursion. The Fibonacci series starts with 0 and 1, and any subsequent value is the sum of the two previous values, so: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, and so on.
Can you also provide a variable template to make your recursive Fibonacci template easier to use?
-
Exercise 26-3: Take your solution for Exercise 26-2 and modify it so that the calculation still happens at compile time, but without the use of any template or function recursion.
-
Exercise 26-4: Write a variadic function template called
push_back_values()accepting a reference to avectorand a variable number of values. The function should use a fold expression to push all the values into the givenvector. Then, write aninsert_values()function template doing the same thing but in terms ofvector::insert(initializer_list<value_type>). What’s the difference with thepush_back_values()implementation? -
Exercise 26-5: Write a
multiply()non-abbreviated function template accepting two template type parametersT1andT2. Use a type trait to verify that both types are arithmetic. If they are, perform the multiplication and return the result. If they are not, throw an exception containing the names of both types. -
Exercise 26-6: Advanced. Transform your solution for Exercise 26-5 to use an abbreviated function template.