Writing Generic Code with Templates
C++ provides language support not only for object-oriented programming but also for generic programming. As discussed in Chapter 6, “Designing for Reuse,” the goal of generic programming is to write reusable code. The fundamental tools for generic programming in C++ are templates. Although not strictly an object-oriented feature, templates can be combined with object-oriented programming for powerful results. Using existing templates, such as those provided by the Standard Library, e.g., std::vector, unique_ptr, and so on, is usually straightforward. However, many programmers consider writing their own templates to be the most difficult part of C++ and, for that reason, tend to avoid writing them. However, as a professional C++ programmer, you need to know how to write class and function templates.
This chapter provides the coding details for fulfilling the design principle of generality discussed in Chapter 6, while Chapter 26, “Advanced Templates,” delves into some of the more advanced template features.
OVERVIEW OF TEMPLATES
Section titled “OVERVIEW OF TEMPLATES”The main programming unit in the procedural paradigm is the procedure or function. Functions are useful primarily because they allow you to write algorithms that are independent of specific values and can thus be reused for many different values. For example, the sqrt() function in C++ calculates the square root of a value supplied by the caller. A square root function that calculates only the square root of one number, such as the number four, would not be particularly useful! The sqrt() function is written in terms of a parameter, which is a stand-in for whatever value the caller passes. Computer scientists say that functions parameterize values.
The object-oriented programming paradigm adds the concept of objects, which group related data and behaviors, but it does not change the way functions and member functions parameterize values.
Templates take the concept of parameterization a step further to allow you to parameterize on types as well as values. Types in C++ include primitives such as int and double, as well as user-defined classes such as SpreadsheetCell and CherryTree. With templates, you can write code that is independent not only of the values it will be given, but also of the types of those values. For example, instead of writing separate stack classes to store ints, Cars, and SpreadsheetCells, you can write one stack class template definition that can be used for any of those types.
Although templates are an amazing language feature, templates in C++ can be syntactically confusing, and thus, many programmers avoid writing templates themselves. However, every professional C++ programmer needs to know how to write them, and every programmer at least needs to know how to use templates, because they are widely used by libraries, such as the C++ Standard Library.
This chapter teaches you about template support in C++ with an emphasis on the aspects that arise in the Standard Library. Along the way, you will learn about some nifty features that you can employ in your programs aside from using the Standard Library.
CLASS TEMPLATES
Section titled “CLASS TEMPLATES”A class template defines a blueprint (= template) for a family of class definitions where the types of some of the variables, return types of member functions, and/or parameters to member functions are specified as template type parameters. Class templates are like construction blueprints. They allow the compiler to build (also known as instantiate) concrete class definitions by replacing template type parameters with concrete types.
Class templates are useful primarily for containers, or data structures, that store objects. You already used class templates often earlier in this book, e.g., std::vector, unique_ptr, string, and so on. This section discusses how to write your own class templates by using a running example of a Grid container. To keep the examples reasonable in length and simple enough to illustrate specific points, different sections of the chapter add features to the Grid container that are not used in subsequent sections.
Writing a Class Template
Section titled “Writing a Class Template”Suppose that you want a generic game board class that you can use as a chessboard, checkers board, tic-tac-toe board, or any other two-dimensional game board. To make it general-purpose, you should be able to store chess pieces, checkers pieces, tic-tac-toe pieces, or any type of game piece.
Coding Without Templates
Section titled “Coding Without Templates”Without templates, the best approach to build a generic game board is to employ polymorphism to store generic GamePiece objects. Then, you could let the pieces for each game inherit from the GamePiece class. For example, in a chess game, ChessPiece would be a derived class of GamePiece. Through polymorphism, the GameBoard, written to store GamePieces, could also store ChessPieces. Because it should be possible to copy a GameBoard, the GameBoard needs to be able to copy GamePieces. This implementation employs polymorphism, so one solution is to add a pure virtual clone() member function to the GamePiece base class, which derived classes must implement to return a copy of a concrete GamePiece. Here is the basic GamePiece interface:
export class GamePiece{ public: virtual ˜GamePiece() = default; virtual std::unique_ptr<GamePiece> clone() const = 0;};GamePiece is an abstract base class. Concrete classes, such as ChessPiece, derive from it and implement the clone() member function:
class ChessPiece : public GamePiece{ public: std::unique_ptr<GamePiece> clone() const override { // Call the copy constructor to copy this instance return std::make_unique<ChessPiece>(*this); }};A GameBoard represents a two-dimensional grid, so one option to store the GamePieces in GameBoard could be a vector of vectors of unique_ptrs. However, that’s not an optimal representation of the data, as the data will be fragmented in memory. It’s better to store a linearized representation of the GamePieces as a single vector of unique_ptrs. Converting a two-dimensional coordinate, say (x,y), to a one-dimensional location in the linearized representation is easily done using the formula x+y*width.
export class GameBoard{ public: explicit GameBoard(std::size_t width = DefaultWidth, std::size_t height = DefaultHeight); GameBoard(const GameBoard& src); // copy constructor virtual ˜GameBoard() = default; // virtual defaulted destructor GameBoard& operator=(const GameBoard& rhs); // assignment operator
// Explicitly default a move constructor and move assignment operator. GameBoard(GameBoard&& src) = default; GameBoard& operator=(GameBoard&& src) = default;
std::unique_ptr<GamePiece>& at(std::size_t x, std::size_t y); const std::unique_ptr<GamePiece>& 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 };
void swap(GameBoard& other) noexcept; private: void verifyCoordinate(std::size_t x, std::size_t y) const;
std::vector<std::unique_ptr<GamePiece>> m_cells; std::size_t m_width { 0 }, m_height { 0 };};export void swap(GameBoard& first, GameBoard& second) noexcept;In this implementation, at() returns a reference to the game piece at a given location instead of a copy of the piece. GameBoard serves as an abstraction of a two-dimensional array, so it should provide array access semantics by returning a reference to the actual object at any location, not a copy of the object. Client code should not store this reference for future use because it might become invalid, for example when the m_cells vector needs to be resized. Instead, client code shall call at() right before using the returned reference. This follows the design philosophy of the Standard Library vector class.
Here are the member function definitions. Note that this implementation uses the copy-and-swap idiom for the assignment operator, and Scott Meyers’ const_cast() pattern to avoid code duplication, both of which are discussed in Chapter 9, “Mastering Classes and Objects.”
GameBoard::GameBoard(size_t width, size_t height) : m_width { width }, m_height { height }{ m_cells.resize(m_width * m_height);}
GameBoard::GameBoard(const GameBoard& src) : GameBoard { 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 (size_t i { 0 }; i < m_cells.size(); ++i) { if (src.m_cells[i]) { m_cells[i] = src.m_cells[i]->clone(); } }}
void GameBoard::verifyCoordinate(size_t x, size_t y) const{ if (x>= m_width) { throw out_of_range { format("x ({}) must be less than width ({}).", x, m_width) }; } if (y>= m_height) { throw out_of_range { format("y ({}) must be less than height ({}).", y, m_height) }; }}
void GameBoard::swap(GameBoard& other) noexcept{ std::swap(m_width, other.m_width); std::swap(m_height, other.m_height); std::swap(m_cells, other.m_cells);}
void swap(GameBoard& first, GameBoard& second) noexcept{ first.swap(second);}
GameBoard& GameBoard::operator=(const GameBoard& rhs){ // Copy-and-swap idiom GameBoard temp { rhs }; // Do all the work in a temporary instance. swap(temp); // Commit the work with only non-throwing operations. return *this;}
const unique_ptr<GamePiece>& GameBoard::at(size_t x, size_t y) const{ verifyCoordinate(x, y); return m_cells[x + y * m_width];}
unique_ptr<GamePiece>& GameBoard::at(size_t x, size_t y){ return const_cast<unique_ptr<GamePiece>&>(as_const(*this).at(x, y));}This GameBoard class works pretty well:
GameBoard chessBoard { 8, 8 };auto pawn { std::make_unique<ChessPiece>() };chessBoard.at(0, 0) = std::move(pawn);chessBoard.at(0, 1) = std::make_unique<ChessPiece>();chessBoard.at(0, 1) = nullptr;A Template Grid Class
Section titled “A Template Grid Class”The GameBoard class in the previous section is nice but insufficient. One problem is that you cannot use GameBoard to store elements by value; it always stores pointers. Another, more serious issue is related to type safety. Each cell in a GameBoard stores a unique_ptr<GamePiece>. Even if you are storing ChessPieces, when you use at() to request a certain piece, you will get back a unique_ptr<GamePiece>. This means you have to downcast the retrieved GamePiece to a ChessPiece to be able to make use of ChessPiece’s specific functionality. Additionally, nothing stops you from mixing all kinds of different GamePiece-derived objects in a GameBoard. For example, suppose there is not only a ChessPiece but also a TicTacToePiece:
class TicTacToePiece : public GamePiece{ public: std::unique_ptr<GamePiece> clone() const override { // Call the copy constructor to copy this instance return std::make_unique<TicTacToePiece>(*this); }};With the polymorphic solution from the previous section, nothing stops you from storing tic-tac-toe pieces and chess pieces on a single game board:
GameBoard gameBoard { 8, 8 };gameBoard.at(0, 0) = std::make_unique<ChessPiece>();gameBoard.at(0, 1) = std::make_unique<TicTacToePiece>();The big problem with this is that you somehow need to remember what is stored at a certain location so that you can perform the correct downcast when you call at().
Another shortcoming of GameBoard is that it cannot be used to store primitive types, such as int or double, because the type stored in a cell must derive from GamePiece.
It would be nice if you could write a generic Grid class that you could use for storing ChessPieces, SpreadsheetCells, ints, doubles, and so on. In C++, you can do this by writing a class template, which is a blueprint for class definitions. In a class template, not all types are known yet. Clients then instantiate the template by specifying the types they want to use. This is called generic programming. The biggest advantage of generic programming is type safety. The types used in instantiated class definitions and their member functions are concrete types, and not abstract base class types, as is the case with the polymorphic solution from the previous section.
Let’s start by looking at how such a Grid class template definition can be written.
The Grid Class Template Definition
Section titled “The Grid Class Template Definition”To understand class templates, it is helpful to examine the syntax. The following example shows how you can modify the GameBoard class to make a parametrized Grid class template. The syntax is explained in detail following the code. Note that the name has changed from GameBoard to Grid. A Grid should also be usable with primitive types such as int and double. That’s why I opted to implement this solution using value semantics without polymorphism, compared to the polymorphic pointer semantics used in the GameBoard implementation. The m_cells container stores actual objects, instead of pointers. A downside of using value semantics compared to pointer semantics is that you cannot have a true empty cell; that is, a cell must always contain some value. With pointer semantics, an empty cell stores nullptr. Luckily, std::optional, introduced in Chapter 1, “A Crash Course in C++ and the Standard Library,” comes to the rescue here. It allows you to use value semantics, while still having a way to represent empty cells.
export template <typename T>class Grid{ public: explicit Grid(std::size_t width = DefaultWidth, std::size_t height = DefaultHeight); virtual ˜Grid() = default;
// Explicitly default a copy constructor and copy assignment operator. Grid(const Grid& src) = default; Grid& operator=(const Grid& rhs) = default;
// Explicitly default a move constructor and move 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 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::optional<T>> m_cells; std::size_t m_width { 0 }, m_height { 0 };};Now that you’ve seen the full class template definition, take another look at it, starting with the first line.
export template <typename T>This first line says that the following class definition is a template on one type, T, and that it’s being exported from the module. The “template <typename T>” part is called the template header. Both template and typename are keywords in C++. As discussed earlier, templates “parameterize” types in the same way that functions “parameterize” values. Just as you use parameter names in functions to represent the arguments that the caller will pass, you use template type parameter names (such as T) in templates to represent the types that the caller will pass as template type arguments. There’s nothing special about the name T—you can use whatever name you want. Traditionally, when a single type is used, it is called T, but that’s just a historical convention, like calling the integer that indexes an array i or j. The template specifier holds for the entire statement, which in this case is the class template definition.
In the earlier GameBoard class, the m_cells data member is a vector of pointers, which requires special code for copying—thus the need for a copy constructor and copy assignment operator. In the Grid class, m_cells is a vector of optional values, so the compiler-generated copy constructor and assignment operator are fine. However, as explained in Chapter 8, “Gaining Proficiency with Classes and Objects,” once you have a user-declared destructor, it’s deprecated for the compiler to implicitly generate a copy constructor or copy assignment operator, so the Grid class template explicitly defaults them. It also explicitly defaults the move constructor and move assignment operator. Here is the explicitly defaulted copy assignment operator:
Grid& operator=(const Grid& rhs) = default;As you can see, the type of the rhs parameter is no longer a const GameBoard&, but a const Grid&. Within a class definition, the compiler interprets Grid as Grid<T> where needed, but if you want, you can explicitly use Grid<T>:
Grid<T>& operator=(const Grid<T>& rhs) = default;However, outside a class definition you must use Grid<T>. When you write a class template, what you used to think of as the class name (Grid) is actually the template name. When you want to talk about actual Grid classes or types, you have to use the template ID, i.e., Grid<T>, which are instantiations of the Grid class template for a certain type, such as int, SpreadsheetCell, or ChessPiece.
Because m_cells is not storing pointers anymore, but optional values, the at() member functions now return optional<T>s instead of unique_ptrs, that is, optionals that can either have a value of type T, or be empty:
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;The Grid Class Template Member Function Definitions
Section titled “The Grid Class Template Member Function Definitions”The template <typename T> template header must precede each member function definition for the Grid class template. The constructor looks like this:
template <typename T>Grid<T>::Grid(std::size_t width, std::size_t height) : m_width { width }, m_height { height }{ m_cells.resize(m_width * m_height);}Note that the name before the :: is Grid<T>, not Grid. The body of the constructor is identical to the GameBoard constructor. The rest of the member function definitions are also similar to their equivalents in the GameBoard class with the exception of the appropriate template header and Grid<T> syntax changes:
template <typename T>void Grid<T>::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>const std::optional<T>& Grid<T>::at(std::size_t x, std::size_t y) const{ verifyCoordinate(x, y); return m_cells[x + y * m_width];}
template <typename T>std::optional<T>& Grid<T>::at(std::size_t x, std::size_t y){ return const_cast<std::optional<T>&>(std::as_const(*this).at(x, y));}Using the Grid Template
Section titled “Using the Grid Template”When you want to create Grid objects, you cannot use Grid alone as a type; you must specify the type that is to be stored in that Grid. Creating concrete instances of class templates for specific types is called template instantiation. Here is an example:
Grid<int> myIntGrid; // Declares a grid that stores ints, // using default arguments for the constructor.Grid<double> myDoubleGrid { 11, 11 }; // Declares an 11x11 Grid of doubles.
myIntGrid.at(0, 0) = 10;int x { myIntGrid.at(0, 0).value_or(0) };
Grid<int> grid2 { myIntGrid }; // Copy constructorGrid<int> anotherIntGrid;anotherIntGrid = grid2; // Assignment operatorNote that the type of myIntGrid, grid2, and anotherIntGrid is Grid<int>. You cannot store SpreadsheetCells or ChessPieces in these grids; the compiler will generate an error if you try to do so.
Note also the use of value_or(). The at() member functions return an optional reference, which can contain a value or not. value_or() returns the value inside the optional if there is a value; otherwise, it returns the argument given to value_or().
The type specification is important; neither of the following two lines compiles:
Grid test; // WILL NOT COMPILEGrid<> test; // WILL NOT COMPILEThe first line causes the compiler to produce an error like “use of class template requires template argument list.” The second line causes an error like “too few template arguments.”
If you want to declare a function that takes a Grid object, you must specify the type stored in that grid as part of the Grid type:
void processIntGrid(Grid<int>& grid) { /* Body omitted for brevity */ }Alternatively, you can use function templates, discussed later in this chapter, to write a function parametrized on the type of the elements in the grid.
The Grid class template can store more than just ints. For example, you can instantiate a Grid that stores SpreadsheetCells:
Grid<SpreadsheetCell> mySpreadsheet;SpreadsheetCell myCell { 1.234 };mySpreadsheet.at(3, 4) = myCell;You can store pointer types as well:
Grid<const char*> myStringGrid;myStringGrid.at(2, 2) = "hello";The type specified can even be another template type:
Grid<vector<int>> gridOfVectors;vector<int> myVector { 1, 2, 3, 4 };gridOfVectors.at(5, 6) = myVector;You can also dynamically allocate Grid objects on the free store:
auto myGridOnFreeStore { make_unique<Grid<int>>(2, 2) }; // 2x2 Grid on free store.myGridOnFreeStore->at(0, 0) = 10;int x { myGridOnFreeStore->at(0, 0).value_or(0) };How the Compiler Processes Templates
Section titled “How the Compiler Processes Templates”To understand the intricacies of templates, you need to learn how the compiler processes template code. When the compiler encounters class template member function definitions, it performs syntax checking, but doesn’t actually compile the templates. It can’t compile template definitions because it doesn’t know for which types they will be used. It’s impossible for a compiler to generate code for something like x = y without knowing the types of x and y. This syntax-checking step is the first step in the two-phase name lookup process.
The second step in the two-phase name lookup process happens when the compiler encounters an instantiation of the template, such as Grid<int>. At that moment, the compiler writes code for an int version of the Grid template by replacing each T in the class template definition with int. When the compiler encounters a different instantiation of the template, such as Grid<SpreadsheetCell>, it writes another version of the Grid class for SpreadsheetCells. The compiler just writes the code that you would write if you didn’t have template support in the language and had to write separate classes for each element type. There’s no magic here; templates just automate an annoying process. If you don’t instantiate a class template for any types in your program, then the class template member function definitions are never compiled.
This instantiation process explains why you need to use the Grid<T> syntax in various places in your definition. When the compiler instantiates the template for a particular type, such as int, it replaces T with int, so that Grid<int> is the type.
Selective/Implicit Instantiation
Section titled “Selective/Implicit Instantiation”For implicit class template instantiations such as the following:
Grid<int> myIntGrid;the compiler always generates code for all virtual member functions of the class template. However, for non-virtual member functions, the compiler generates code only for those non-virtual member functions that are actually called. For example, given the earlier Grid class template, suppose that you write this code (and only this code) in main():
Grid<int> myIntGrid;myIntGrid.at(0, 0) = 10;The compiler generates only the zero-argument constructor, the destructor, and the non-const at() member function for an int version of Grid. It does not generate other member functions like the copy constructor, the assignment operator, or getHeight(). This is called selective instantiation.
Explicit Instantiation
Section titled “Explicit Instantiation”The danger exists that there are compilation errors in some class template member functions that go unnoticed with implicit instantiations. Unused member functions of class templates can contain syntax errors, as these will not be compiled. This makes it hard to test all code for syntax errors. You can force the compiler to generate code for all member functions, virtual and non-virtual, by using explicit template instantiations. Here’s an example:
template class Grid<string>;When using explicit template instantiations, don’t just try to instantiate the class template with basic types like int, but try it with more complicated types like string, if those are accepted by the class template.
Template Requirements on Types
Section titled “Template Requirements on Types”When you write code that is independent of types, you must assume certain things about those types. For example, in the Grid class template, you assume that the element type (represented by T) is destructible, copy/move constructible, and copy/move assignable.
When the compiler attempts to instantiate a template with types that do not support all the operations used by class template member functions that are called, the code will not compile, and the error messages will often be quite obscure. However, even if the types you want to use don’t support the operations required by all the member functions of the class template, you can exploit selective instantiation to use some member functions but not others.
You can use concepts to write requirements for template parameters that the compiler can interpret and validate. The compiler can generate more readable errors if the template arguments passed to instantiate a template do not satisfy these requirements. Concepts are discussed later in this chapter.
Distributing Template Code Between Files
Section titled “Distributing Template Code Between Files”With class templates, both the class template definition and the member function definitions must be available to the compiler from any source file that uses them. There are several mechanisms to accomplish this.
Member Function Definitions in Same File as Class Template Definition
Section titled “Member Function Definitions in Same File as Class Template Definition”You can place the member function definitions directly in the module interface file where you define the class template itself. When you import this module in another source file where you use the template, the compiler will have access to all the code it needs. This mechanism is used for the previous Grid implementation.
Member Function Definitions in Separate File
Section titled “Member Function Definitions in Separate File”Alternatively, you can place the class template member function definitions in a separate module interface partition file. You then also need to put the class template definition in its own module interface partition. For example, the primary module interface file for the Grid class template could look like this:
export module grid;
export import :definition;export import :implementation;This imports and exports two module interface partitions: definition and implementation. The class template definition is defined in the definition partition:
export module grid:definition;
import std;
export template <typename T> class Grid { … };The implementations of the member functions are in the implementation partition, which also needs to import the definition partition because it needs the Grid class template definition:
export module grid:implementation;
import :definition;import std;
export template <typename T>Grid<T>::Grid(std::size_t width, std::size_t height) : m_width { width }, m_height { height }{ /* … */ }// Remainder omitted for brevity.Template Parameters
Section titled “Template Parameters”In the Grid example, the Grid class template has one template parameter: the type that is stored in the grid. When you write the class template, you specify the parameter list inside the angle brackets, like this:
template <typename T>This parameter list is similar to the parameter list of functions. As with functions, you can write a class template with as many template parameters as you want. Additionally, these parameters don’t have to be types, and they can have default values.
Non-type Template Parameters
Section titled “Non-type Template Parameters”Non-type template parameters are “normal” parameters such as ints and pointers—the kind of parameters which you’re familiar with from functions. However, non-type template parameters can only be integral types (char, int, long, and so on), enumerations, pointers, references, std::nullptr_t, auto, auto&, auto*, floating-point types, and class types. The latter, however, come with a lot of limitations, not further discussed in this text. Remember that templates are instantiated at compile time; hence, arguments for non-type template parameters are evaluated at compile time. That means such arguments must be literals or compile-time constants.
In the Grid class template, you could use non-type template parameters to specify the height and width of the grid instead of specifying them in the constructor. The principal advantage of using non-type template parameters instead of constructor parameters is that the values are known before the code is compiled. Recall that the compiler generates code for template instantiations by substituting the template parameters before compiling. Thus, you can use a normal two-dimensional array in the following implementation instead of a linearized representation using a vector that is dynamically resized. Here is the new class template definition with the changes highlighted:
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 copy assignment operator. Grid(const Grid& src) = default; Grid& operator=(const Grid& rhs) = default;
// Explicitly default a move constructor and move 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];};The template parameter list now has three parameters: the type of objects stored in the grid, and the width and height of the grid. The width and height are used to create a two-dimensional array to store the objects. Here are the class template member function definitions:
template <typename T, std::size_t WIDTH, std::size_t HEIGHT>void Grid<T, WIDTH, HEIGHT>::verifyCoordinate(std::size_t x, std::size_t y) const{ if (x >= WIDTH) { throw std::out_of_range { std::format("x ({}) must be less than width ({}).", x, WIDTH) }; } if (y >= HEIGHT) { throw std::out_of_range { std::format("y ({}) must be less than height ({}).", y, HEIGHT) }; }}
template <typename T, std::size_t WIDTH, std::size_t HEIGHT>const std::optional<T>& Grid<T, WIDTH, HEIGHT>::at( std::size_t x, std::size_t y) const{ verifyCoordinate(x, y); return m_cells[x][y];}
template <typename T, std::size_t WIDTH, std::size_t HEIGHT>std::optional<T>& Grid<T, WIDTH, HEIGHT>::at(std::size_t x, std::size_t y){ return const_cast<std::optional<T>&>(std::as_const(*this).at(x, y));}Note that wherever you previously specified Grid<T> you must now specify Grid<T,WIDTH,HEIGHT> to specify the three template parameters.
You can instantiate this template and use it as follows:
Grid<int, 10, 10> myGrid;Grid<int, 10, 10> anotherGrid;myGrid.at(2, 3) = 42;anotherGrid = myGrid;println("{}", anotherGrid.at(2, 3).value_or(0));This code seems great, but unfortunately, there are more restrictions than you might initially expect. First, you can’t use a non-constant integer to specify the height or width. The following code doesn’t compile:
size_t height { 10 };Grid<int, 10, height> testGrid; // DOES NOT COMPILEIf you define height as a constant, it compiles:
const size_t height { 10 };Grid<int, 10, height> testGrid; // Compiles and worksconstexpr functions with the correct return type also work. For example, if you have a constexpr function returning a size_t, you can use it to initialize the height template parameter:
constexpr size_t getHeight() { return 10; }…Grid<double, 2, getHeight()> myDoubleGrid;A second restriction might be more significant. Now that the width and height are template parameters, they are part of the type of each grid. That means Grid<int,10,10> and Grid<int,10,11> are two different types. You can’t assign an object of one type to an object of the other, and variables of one type can’t be passed to functions that expect variables of another type.
Default Values for Template Parameters
Section titled “Default Values for Template Parameters”If you continue the approach of making height and width template parameters, you might want to provide defaults for the height and width non-type template parameters just as you did previously in the constructor of the Grid<T> class template. C++ allows you to provide defaults for template parameters with a similar syntax. While you are at it, you could also provide a default for the T type parameter. Here is the class definition:
export template <typename T = int, std::size_t WIDTH = 10, std::size_t HEIGHT = 10>class Grid{ // Remainder is identical to the previous version};You don’t specify the default values for T, WIDTH, and HEIGHT in the template header for the member function definitions. For example, here is the implementation of at():
template <typename T, std::size_t WIDTH, std::size_t HEIGHT>const std::optional<T>& Grid<T, WIDTH, HEIGHT>::at( std::size_t x, std::size_t y) const{ verifyCoordinate(x, y); return m_cells[x][y];}With these changes, you can instantiate a Grid without any template parameters, with only the element type, the element type and the width, or the element type, width, and height:
Grid<> myIntGrid;Grid<int> myGrid;Grid<int, 5> anotherGrid;Grid<int, 5, 5> aFourthGrid;Note that if you don’t specify any class template parameters, you still need to specify an empty set of angle brackets. For example, the following does not compile!
Grid myIntGrid;The rules for default arguments in class template parameter lists are the same as for functions; that is, you can provide defaults for parameters in order starting from the right.
Class Template Argument Deduction
Section titled “Class Template Argument Deduction”With class template argument deduction, the compiler can automatically deduce the template type parameters from the arguments passed to a class template constructor.
For example, the Standard Library has a class template called std::pair, defined in <utility> and introduced in Chapter 1. A pair stores exactly two values of two possibly different types, which you normally would have to specify as the template type parameters. Here’s an example:
pair<int, double> pair1 { 1, 2.3 };To avoid having to write the template type parameters explicitly, a helper function template called std::make_pair() is available. Details of writing your own function templates are discussed later in this chapter. Function templates have always supported the automatic deduction of template type parameters based on the arguments passed to the function template. Thus, make_pair() is capable of automatically deducing the template type parameters based on the values passed to it. For example, the compiler deduces pair<int, double> for the following call:
auto pair2 { make_pair(1, 2.3) };With class template argument deduction (CTAD), such helper function templates are no longer necessary. The compiler now automatically deduces the template type parameters based on the arguments passed to a constructor. For the pair class template, you can simply write the following code:
pair pair3 { 1, 2.3 }; // pair3 has type pair<int, double>Of course, this works only when all template type parameters of a class template either have default values or are used as parameters in the constructor so that they can be deduced.
Note that an initializer is required for CTAD to work. The following is illegal:
pair pair4;A lot of the Standard Library classes support CTAD, for example, vector, array, and so on.
User-Defined Deduction Guides
Section titled “User-Defined Deduction Guides”You can also write your own user-defined deduction guides to help the compiler. These allow you to write rules for how the template type parameters have to be deduced. The following is an example demonstrating their use.
Suppose you have this SpreadsheetCell class template:
template <typename T>class SpreadsheetCell{ public: explicit SpreadsheetCell(T t) : m_content { move(t) } { } const T& getContent() const { return m_content; } private: T m_content;};Thanks to CTAD, you can create a SpreadsheetCell with an std::string type. The deduced type is SpreadsheetCell<string>:
string myString { "Hello World!" };SpreadsheetCell cell { myString };However, if you pass a const char* to the SpreadsheetCell constructor, then type T is deduced as const char*, which is not what you want! You can create the following user-defined deduction guide to make sure T is deduced as std::string when passing a const char* as argument to the constructor:
SpreadsheetCell(const char*) -> SpreadsheetCell<std::string>;This guide has to be defined outside the class definition but inside the same namespace as the SpreadsheetCell class.
The general syntax is as follows. The explicit keyword is optional and behaves the same as explicit for constructors. Such deduction guides are, more often than not, templates as well.
template <…>explicit TemplateName(Parameters) -> DeducedTemplate<…>;Member Function Templates
Section titled “Member Function Templates”C++ allows you to parametrize individual member functions of a class. Such member functions are called member function templates and can be inside a normal class or in a class template. When you write a member function template, you are actually writing many different versions of that member function for many different types. Member function templates are useful for assignment operators and copy constructors in class templates.
Virtual member functions and destructors cannot be member function templates.
Consider the original Grid template with only one template parameter: the element type. You can instantiate grids of many different types, such as ints and doubles:
Grid<int> myIntGrid;Grid<double> myDoubleGrid;However, Grid<int> and Grid<double> are two different types. If you write a function that takes an object of type Grid<double>, you cannot pass a Grid<int>. Even though you know that the elements of an int grid could be copied to the elements of a double grid, because ints can be converted into doubles, you cannot assign an object of type Grid<int> to one of type Grid<double> or construct a Grid<double> from a Grid<int>. Neither of the following two lines compiles:
myDoubleGrid = myIntGrid; // DOES NOT COMPILEGrid<double> newDoubleGrid { myIntGrid }; // DOES NOT COMPILEThe problem is that the copy constructor and assignment operator for the Grid template are as follows:
Grid(const Grid& src);Grid& operator=(const Grid& rhs);which are equivalent to:
Grid(const Grid<T>& src);Grid<T>& operator=(const Grid<T>& rhs);The Grid copy constructor and operator= both take a reference to a const Grid<T>. When you instantiate a Grid<double> and try to call the copy constructor and operator=, the compiler generates member functions with these prototypes:
Grid(const Grid<double>& src);Grid<double>& operator=(const Grid<double>& rhs);There are no constructors or operator= that take a Grid<int> within the generated Grid<double> class.
Luckily, you can rectify this oversight by adding parametrized versions of the copy constructor and assignment operator to the Grid class template to generate member functions that will convert from one grid type to another. Here is the new Grid class template definition:
export template <typename T>class Grid{ public: template <typename E> Grid(const Grid<E>& src);
template <typename E> Grid& operator=(const Grid<E>& rhs);
void swap(Grid& other) noexcept;
// Omitted for brevity};The original copy constructor and copy assignment operator cannot be removed. The compiler will not call these new parametrized copy constructor and parametrized copy assignment operator if E equals T.
Examine the new parametrized copy constructor first:
template <typename E>Grid(const Grid<E>& src);You can see that there is another template header with a different typename, E (short for “element”). The class is parametrized on one type, T, and the new copy constructor is additionally parametrized on a different type, E. This twofold parametrization allows you to copy grids of one type to another. Here is the definition of the new copy constructor:
template <typename T>template <typename E>Grid<T>::Grid(const Grid<E>& src) : Grid { src.getWidth(), src.getHeight() }{ // 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_width; ++i) { for (std::size_t j { 0 }; j < m_height; ++j) { at(i, j) = src.at(i, j); } }}As you can see, you must declare the class template header (with the T parameter) before the member template header (with the E parameter). You can’t combine them like this:
template <typename T, typename E> // Wrong for nested template constructor!Grid<T>::Grid(const Grid<E>& src)In addition to the extra template header before the constructor definition, note that you must use the public accessor member functions getWidth(), getHeight(), and at() to access the elements of src. That’s because the object you’re copying to is of type Grid<T>, and the object you’re copying from is of type Grid<E>. They are not the same type, so you must use public member functions.
The swap() member function is straightforward:
template <typename T>void Grid<T>::swap(Grid& other) noexcept{ std::swap(m_width, other.m_width); std::swap(m_height, other.m_height); std::swap(m_cells, other.m_cells);}The parametrized assignment operator takes a const Grid<E>& but returns a Grid<T>&:
template <typename T>template <typename E>Grid<T>& Grid<T>::operator=(const Grid<E>& rhs){ // Copy-and-swap idiom Grid<T> temp { rhs }; // Do all the work in a temporary instance. swap(temp); // Commit the work with only non-throwing operations. return *this;}The implementation of this assignment operator uses the copy-and-swap idiom introduced in Chapter 9. The swap() member function can only swap Grids of the same type, but that’s OK because this parametrized assignment operator first converts a given Grid<E> to a Grid<T> called temp using the parametrized copy constructor. Afterward, it uses the swap() member function to swap this temporary Grid<T> with this, which is also of type Grid<T>.
Member Function Templates with Non-type Template Parameters
Section titled “Member Function Templates with Non-type Template Parameters”A major problem with the earlier Grid class template with integer template parameters for HEIGHT and WIDTH is that the height and width become part of the types. This restriction prevents you from assigning a grid with one height and width to a grid with a different height and width. In some cases, however, it’s desirable to assign or copy a grid of one size to a grid of a different size. Instead of making the destination object a perfect clone of the source object, you would copy only those elements from the source array that fit in the destination array, padding the destination array with default values if the source array is smaller in either dimension. With member function templates for the assignment operator and copy constructor, you can do exactly that, thus allowing assignment and copying of different-sized grids. Here is the class definition:
export template <typename T, std::size_t WIDTH = 10, std::size_t HEIGHT = 10>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 move assignment operator. Grid(Grid&& src) = default; Grid& operator=(Grid&& rhs) = default;
template <typename E, std::size_t WIDTH2, std::size_t HEIGHT2> Grid(const Grid<E, WIDTH2, HEIGHT2>& src);
template <typename E, std::size_t WIDTH2, std::size_t HEIGHT2> Grid& operator=(const Grid<E, WIDTH2, HEIGHT2>& rhs);
void swap(Grid& other) noexcept;
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];};This new definition includes member function templates for the copy constructor and assignment operator, plus a helper member function swap(). Note that the non-parametrized copy constructor and assignment operator are explicitly defaulted (because of the user-declared destructor). They simply copy or assign m_cells from the source to the destination, which is exactly the semantics you want for two grids of the same size.
Here is the parametrized copy constructor:
template <typename T, std::size_t WIDTH, std::size_t HEIGHT>template <typename E, std::size_t WIDTH2, std::size_t HEIGHT2>Grid<T, WIDTH, HEIGHT>::Grid(const Grid<E, WIDTH2, HEIGHT2>& src){ for (std::size_t i { 0 }; i < WIDTH; ++i) { for (std::size_t j { 0 }; j < HEIGHT; ++j) { if (i < WIDTH2 && j < HEIGHT2) { m_cells[i][j] = src.at(i, j); } else { m_cells[i][j].reset(); } } }}Note that this copy constructor copies only WIDTH and HEIGHT elements in the x and y dimensions, respectively, from src, even if src is bigger than that. If src is smaller in either dimension, the std::optional objects in the extra spots are reset using the reset() member function.
Here are the implementations of swap() and operator=:
template <typename T, std::size_t WIDTH, std::size_t HEIGHT>void Grid<T, WIDTH, HEIGHT>::swap(Grid& other) noexcept{ std::swap(m_cells, other.m_cells);}
template <typename T, std::size_t WIDTH, std::size_t HEIGHT>template <typename E, std::size_t WIDTH2, std::size_t HEIGHT2>Grid<T, WIDTH, HEIGHT>& Grid<T, WIDTH, HEIGHT>::operator=( const Grid<E, WIDTH2, HEIGHT2>& rhs){ // Copy-and-swap idiom Grid<T, WIDTH, HEIGHT> temp { rhs }; // Do all the work in a temp instance. swap(temp); // Commit the work with only non-throwing operations. return *this;} Using Member Function Templates with Explicit Object Parameters to Avoid Code Duplication
Section titled “ Using Member Function Templates with Explicit Object Parameters to Avoid Code Duplication”Our running example of the Grid class template with a single template type parameter T contains two overloads of an at() member function, const and non-const. As a reminder:
export template <typename T>class Grid{ public: 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; // Remainder omitted for brevity};Their implementations use Scott Meyers’ const_cast() pattern to avoid code duplication:
template <typename T>const std::optional<T>& Grid<T>::at(std::size_t x, std::size_t y) const{ verifyCoordinate(x, y); return m_cells[x + y * m_width];}
template <typename T>std::optional<T>& Grid<T>::at(std::size_t x, std::size_t y){ return const_cast<std::optional<T>&>(std::as_const(*this).at(x, y));}Although there is no code duplication, you still need to define both the const and non-const overloads explicitly. Starting with C++23, you can use an explicit object parameter (see Chapter 8) to avoid having to provide the two overloads explicitly. The trick is to turn the at() member function into a member function template where the type of the explicit object parameter self is itself a template type parameter, Self, and thus deduced automatically. This feature is called deducing this. Here is such a declaration:
export template <typename T>class Grid{ public: template <typename Self> auto&& at(this Self&& self, std::size_t x, std::size_t y); // Remainder omitted for brevity};The implementation uses a forwarding reference, Self&&; see the following note. Such a forwarding reference can bind to Grid<T>&, const Grid<T>&, and Grid<T>&&.
Here is the implementation. Remember from Chapter 8 that in the body of a member function using an explicit object parameter, you need to use the explicit object parameter, self in this case, to get access to the object; there is no this pointer.
template <typename T>template <typename Self>auto&& Grid<T>::at(this Self&& self, std::size_t x, std::size_t y){ self.verifyCoordinate(x, y); return std::forward_like<Self>(self.m_cells[x + y * self.m_width]);}The implementation uses std::forward_like<Self>(x) introduced in C++23. This returns a reference to x with similar properties as Self&&. Thus, since the type of the elements of m_cells is optional<T>, the following holds:
- If
Self&&is bound to aGrid<T>&, the return type will be anoptional<T>&. - If
Self&&is bound to aconst Grid<T>&, the return type will be aconst optional<T>&. - If
Self&&is bound to aGrid<T>&&, the return type will be anoptional<T>&&.
To summarize, with a combination of member function templates, explicit object parameters, forwarding references, and forward_like(), it becomes possible to declare and define just a single member function template that provides both const and non-const instantiations.
Class Template Specialization
Section titled “Class Template Specialization”You can provide alternate implementations of class templates for specific types. For example, you might decide that the Grid behavior for const char*s (C-style strings) doesn’t make sense. A Grid<const char*> will store its elements in a vector<optional<const char*>>. The copy constructor and assignment operator will perform shallow copies of this const char* pointer type. For const char*s, it makes more sense to do a deep copy of strings. The easiest solution for this is to write an alternative implementation specifically for const char*s, which converts them to C++ strings and stores them in a vector<optional<string>>.
Alternate implementations of templates are called template specializations. You might find the syntax to be a little weird at first. When you write a class template specialization, you must specify that it’s based on a template and that you are writing a version of the template for a particular type. Here is the syntax for a const char* specialization for Grid. For this implementation, the original Grid class template is moved to a module interface partition called main, while the specialization is in a module interface partition called string.
export module grid:string;import std;// When the template specialization is used, the original template must be// visible too.import :main;
export template <>class Grid<const char*>{ 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;
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 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::optional<std::string>> m_cells; std::size_t m_width { 0 }, m_height { 0 };};Note that you don’t refer to any type variable, such as T, in the specialization: you work directly with const char*s and strings. One obvious question at this point is why this class still has a template header. That is, what good is the following syntax?
template <>class Grid<const char*>This syntax tells the compiler that this class is a const char* specialization of the Grid class template. Suppose that you didn’t use that syntax and just tried to write this:
class GridThe compiler wouldn’t let you do that because there is already a class template named Grid (the original class template). Only by specializing it can you reuse the name. The main benefit of specializations is that they can be invisible to the user. When a user creates a Grid of ints or SpreadsheetCells, the compiler generates code from the original Grid template. When the user creates a Grid of const char*s, the compiler uses the const char* specialization. This can all be “behind the scenes.”
The primary module interface file simply imports and exports both module interface partitions:
export module grid;
export import :main;export import :string;The specialization can be tested as follows:
Grid<int> myIntGrid; // Uses original Grid template.Grid<const char*> stringGrid1 { 2, 2 }; // Uses const char* specialization.
const char* dummy { "dummy" };stringGrid1.at(0, 0) = "hello";stringGrid1.at(0, 1) = dummy;stringGrid1.at(1, 0) = dummy;stringGrid1.at(1, 1) = "there";
Grid<const char*> stringGrid2 { stringGrid1 };When you specialize a class template, you don’t “inherit” any code; specializations are not like derivations. You must rewrite the entire implementation of the class. There is no requirement that you provide member functions with the same names or behavior. As an example, the const char* specialization of Grid implements the at() member functions by returning an optional<string>, not an optional<const char*>. As a matter of fact, you could write a completely different class with no relation to the original. Of course, that would abuse the template specialization ability, and you shouldn’t do it without good reason. Here are the implementations for the member functions of the const char* specialization. Unlike in the class template definition, you do not repeat the template header, template<>, before each member function definition.
Grid<const char*>::Grid(std::size_t width, std::size_t height) : m_width { width }, m_height { height }{ m_cells.resize(m_width * m_height);}
void Grid<const char*>::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) }; }}
const std::optional<std::string>& Grid<const char*>::at( std::size_t x, std::size_t y) const{ verifyCoordinate(x, y); return m_cells[x + y * m_width];}
std::optional<std::string>& Grid<const char*>::at(std::size_t x, std::size_t y){ return const_cast<std::optional<std::string>&>( std::as_const(*this).at(x, y));}This section discussed how to use class template specialization to write a special implementation for a class template, with all template type parameters replaced with specific types. This is called full template specialization. Such a full class template specialization is no longer a class template itself but a class definition. Chapter 26, “Advanced Templates,” continues the discussion of class template specialization with a more advanced feature called partial specialization.
Deriving from Class Templates
Section titled “Deriving from Class Templates”You can inherit from class templates. If the derived class inherits from the template itself, it must be a template as well. Alternatively, you can derive from a specific instantiation of the class template, in which case your derived class does not need to be a template. As an example of the former, suppose you decide that the generic Grid class doesn’t provide enough functionality to use as a game board. Specifically, you would like to add a move() member function to the game board that moves a piece from one location on the board to another. Here is the class definition for the GameBoard template:
import grid;
export template <typename T>class GameBoard : public Grid<T>{ public: // Inherit constructors from Grid<T>. using Grid<T>::Grid;
void move(std::size_t xSrc, std::size_t ySrc, std::size_t xDest, std::size_t yDest);};This GameBoard template derives from the Grid template and thereby inherits all its functionality. You don’t need to rewrite at(), getHeight(), or any of the other member functions. You also don’t need to add a copy constructor, operator=, or destructor, because you don’t have any dynamically allocated memory in GameBoard. Additionally, GameBoard explicitly inherits the constructors from the base class, Grid<T>. Inheriting constructors from base classes is explained in Chapter 10, “Discovering Inheritance Techniques.”
The inheritance syntax looks normal, except that the base class is Grid<T>, not Grid. The reason for this syntax is that the GameBoard template doesn’t really derive from the generic Grid template. Rather, each instantiation of the GameBoard template for a specific type derives from the Grid instantiation for that same type. For example, if you instantiate a GameBoard with a ChessPiece type, then the compiler generates code for a Grid<ChessPiece> as well. The : public Grid<T> syntax says that this class inherits from whatever Grid instantiation makes sense for the T type parameter.
Here is the implementation of the move() member function:
template <typename T>void GameBoard<T>::move(std::size_t xSrc, std::size_t ySrc, std::size_t xDest, std::size_t yDest){ Grid<T>::at(xDest, yDest) = std::move(Grid<T>::at(xSrc, ySrc)); Grid<T>::at(xSrc, ySrc).reset(); // Reset source cell // Or: // this->at(xDest, yDest) = std::move(this->at(xSrc, ySrc)); // this->at(xSrc, ySrc).reset();}You can use the GameBoard template as follows:
GameBoard<ChessPiece> chessboard { 8, 8 };ChessPiece pawn;chessBoard.at(0, 0) = pawn;chessBoard.move(0, 0, 0, 1);Inheritance vs. Specialization
Section titled “Inheritance vs. Specialization”Some programmers find the distinction between template inheritance and template specialization confusing. The following table summarizes the differences:
| INHERITANCE | SPECIALIZATION | |
|---|---|---|
| Reuses code? | Yes: Derived classes contain all base class data members and member functions. | No: You must rewrite all required code in the specialization. |
| Reuses name? | No: The derived class name must be different from the base class name. | Yes: The specialization must have the same name as the original. |
| Supports polymorphism? | Yes: Objects of the derived class can stand in for objects of the base class. | No: Each instantiation of a template for a type is a different type. |
Alias Templates
Section titled “Alias Templates”Chapter 1 introduces the concept of type aliases and typedefs. They allow you to give other names to specific types. To refresh your memory, you could, for example, write the following type alias to give a second name to type int:
using MyInt = int;Similarly, you can use a type alias to give another name to a class template. Suppose you have the following class template:
template <typename T1, typename T2>class MyClassTemplate { /* … */ };You can define the following type alias in which you specify both class template type parameters:
using OtherName = MyClassTemplate<int, double>;A typedef can also be used instead of such a type alias.
Additionally, it’s also possible to specify only some of the types and keep the other types as template type parameters. This is called an alias template. Here’s an example:
template <typename T1>using OtherName = MyClassTemplate<T1, double>;This is something you cannot do with a typedef.
FUNCTION TEMPLATES
Section titled “FUNCTION TEMPLATES”You can also write templates for stand-alone functions. The syntax is similar to the syntax for class templates. For example, you could write the following generic function to find a value in an array and return its index:
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.}The Find() function template can work on arrays of any type. For example, you could use it to find the index of an int in an array of ints or a SpreadsheetCell in an array of SpreadsheetCells.
You can call the function in two ways: explicitly specifying the template type parameter with angle brackets or omitting the type and letting the compiler deduce the type parameter from the arguments. Here are some examples:
int myInt { 3 }, intArray[] {1, 2, 3, 4};const size_t sizeIntArray { size(intArray) };
optional<size_t> res;res = Find(myInt, intArray, sizeIntArray); // calls Find<int> by deduction.res = Find<int>(myInt, intArray, sizeIntArray); // calls Find<int> explicitly.if (res) { println("{}", *res); }else { println("Not found"); }
double myDouble { 5.6 }, doubleArray[] {1.2, 3.4, 5.7, 7.5};const size_t sizeDoubleArray { size(doubleArray) };
// calls Find<double> by deduction.res = Find(myDouble, doubleArray, sizeDoubleArray);// calls Find<double> explicitly.res = Find<double>(myDouble, doubleArray, sizeDoubleArray);if (res) { println("{}", *res); }else { println("Not found"); }
//res = Find(myInt, doubleArray, sizeDoubleArray); // DOES NOT COMPILE! // Arguments are different types.// calls Find<double> explicitly, even with myInt.res = Find<double>(myInt, doubleArray, sizeDoubleArray);
SpreadsheetCell cell1 { 10 }SpreadsheetCell cellArray[] { SpreadsheetCell { 4 }, SpreadsheetCell { 10 } };const size_t sizeCellArray { size(cellArray) };
res = Find(cell1, cellArray, sizeCellArray);res = Find<SpreadsheetCell>(cell1, cellArray, sizeCellArray);The previous implementation of the Find() function template requires the size of the array as one of the parameters. Sometimes the compiler knows the exact size of an array, for example, for stack-based arrays. It would be nice to be able to call Find() with such arrays without the need to pass it the size of the array. This can be accomplished by adding the following function template. The implementation just forwards the call to the previous Find() function template. This also demonstrates that function templates can take non-type parameters, just like class templates.
template <typename T, size_t N>optional<size_t> Find(const T& value, const T(&arr)[N]){ return Find(value, arr, N);}The syntax of this overload of Find() looks a bit strange, but its use is straightforward, as in this example:
int myInt { 3 }, intArray[] {1, 2, 3, 4};optional<size_t> res { Find(myInt, intArray) };Like class template member function definitions, function template definitions (not just the prototypes) must be available to all source files that use them. Thus, you should put the definitions in module interface files and export them if more than one source file uses them.
Finally, template parameters of function templates can have defaults, just like class templates.
Function Overloads vs. Function Template
Section titled “Function Overloads vs. Function Template”There are two options when you want to provide a function that can work with different data types: provide function overloads or provide a function template. How do you choose between those two options?
When writing a function that should work with different data types and for which the body of the function is the same for all data types, provide a function template. If the body of the function is different for every data type, provide function overloads.
Function Template Overloading
Section titled “Function Template Overloading”In theory, the C++ language allows you to write function template specializations, just as you can write class template specializations. However, you rarely want to do this because such function template specializations do not participate in overload resolution and hence might behave unexpectedly.
Instead, you can overload function templates with either non-template functions or other function templates. For example, you might want to write a Find() overload for const char* C-style strings that compares them with strcmp() (see Chapter 2, “Working with Strings and String Views”) instead of operator==, as == would only compare pointers, not the actual strings. Here is such an overload:
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.}This function overload can be used as follows:
// Using an array for word to make sure no literal pooling happens, see Chapter 2.const char word[] { "two" };const char* words[] { "one", "two", "three", "four" };const size_t sizeWords { size(words) };optional<size_t> res { Find(word, words, sizeWords) }; // Calls non-template Find.if (res) { println("{}", *res); }else { println("Not found"); }The call to Find() correctly finds the string “two” at index 1.
If you do explicitly specify the template type parameter as follows, then the function template will be called with T=const char*, and not the overload for const char*:
res = Find<const char*>(word, words, sizeWords);This call of Find() does not find any matches, as it doesn’t compare the actual strings, but just pointers.
When the overload resolution process of the compiler results in two possible candidates, one being a function template, the other being a non-template function, then the compiler always prefers to use the non-template function.
Function Templates as Friends of Class Templates
Section titled “Function Templates as Friends of Class Templates”Function templates are useful when you want to overload operators in a class template. For example, you might want to overload the addition operator (operator+) for the Grid class template to be able to add two grids together. The result will be a Grid with the same size as the smallest Grid of the two operands. Corresponding cells are added together only if both cells contain an actual value. Suppose you want to make your operator+ a stand-alone function template. The definition, which should go in the Grid.cppm module interface file, looks as follows. The implementation uses std::min(), defined in <algorithm>, to return the minimum value of two given arguments:
export template <typename T>Grid<T> operator+(const Grid<T>& lhs, const Grid<T>& rhs){ std::size_t minWidth { std::min(lhs.m_width, rhs.m_width) }; std::size_t minHeight { std::min(lhs.m_height, rhs.m_height) };
Grid<T> result { minWidth, minHeight }; for (std::size_t y { 0 }; y < minHeight; ++y) { for (std::size_t x { 0 }; x < minWidth; ++x) { const auto& leftElement { lhs.at(x, y) }; const auto& rightElement { rhs.at(x, y) }; if (leftElement.has_value() && rightElement.has_value()) { result.at(x, y) = leftElement.value() + rightElement.value(); } } } return result;}To query whether an optional contains an actual value, you use the has_value() member function, while value() is used to retrieve this value.
This function template works on any Grid, as long as there is an addition operator for the type of elements stored in the grid. The only problem with this implementation is that it accesses private members m_width and m_height of the Grid class. The obvious solution is to use the public getWidth() and getHeight() member functions, but let’s see how you can make a function template a friend of a class template. For this example, you can make the operator a friend of the Grid class template. However, both Grid and the operator+ are templates. What you really want is for each instantiation of operator+ for a particular type T to be a friend of the Grid template instantiation for that same type. The syntax looks like this:
export template <typename T>class Grid{ public: friend Grid operator+<T>(const Grid& lhs, const Grid& rhs); // Omitted for brevity};This friend declaration is tricky: you’re saying that, for an instance of the class template with type T, the T instantiation of operator+ is a friend. In other words, there is a one-to-one mapping of friends between the class instantiations and the function instantiations. Note particularly the explicit template specification <T> on operator+. This syntax tells the compiler that operator+ is itself a template.
This friend operator+ can be tested as follows. The following code first defines two helper function templates: fillGrid(), which fills any Grid with increasing numbers, and printGrid(), which prints any Grid to the console.
template <typename T> void fillGrid(Grid<T>& grid){ T index { 0 }; for (size_t y { 0 }; y < grid.getHeight(); ++y) { for (size_t x { 0 }; x < grid.getWidth(); ++x) { grid.at(x, y) = ++index; } }}
template <typename T> void printGrid(const Grid<T>& grid){ for (size_t y { 0 }; y < grid.getHeight(); ++y) { for (size_t x { 0 }; x < grid.getWidth(); ++x) { const auto& element { grid.at(x, y) }; if (element.has_value()) { print("{}\t", element.value()); } else { print("n/a\t"); } } println(""); }}
int main(){ Grid<int> grid1 { 2, 2 }; Grid<int> grid2 { 3, 3 }; fillGrid(grid1); println("grid1:"); printGrid(grid1); fillGrid(grid2); println("\ngrid2:"); printGrid(grid2); auto result { grid1 + grid2 }; println("\ngrid1 + grid2:"); printGrid(result);}More on Template Type Parameter Deduction
Section titled “More on Template Type Parameter Deduction”The compiler deduces the type of function template parameters based on the arguments passed to the function template. Template parameters that cannot be deduced have to be specified explicitly.
For example, the following add() function template requires three template parameters: the type of the return value and the types of the two operands:
template <typename RetType, typename T1, typename T2>RetType add(const T1& t1, const T2& t2) { return t1 + t2; }You can call this function template specifying all three parameters as follows:
auto result { add<long long, int, int>(1, 2) };However, because the template parameters T1 and T2 are parameters to the function, the compiler can deduce those two parameters, so you can call add() by only specifying the type for the return value:
auto result { add<long long>(1, 2) };This works only when the parameters to deduce are last in the list of parameters. Suppose the function template is defined as follows:
template <typename T1, typename RetType, typename T2>RetType add(const T1& t1, const T2& t2) { return t1 + t2; }You have to specify RetType, because the compiler cannot deduce that type. However, because RetType is the second parameter, you have to explicitly specify T1 as well:
auto result { add<int, long long>(1, 2) };You can also provide a default value for the return type template parameter so that you can call add() without specifying any types:
template <typename RetType = long long, typename T1, typename T2>RetType add(const T1& t1, const T2& t2) { return t1 + t2; }…auto result { add(1, 2) };Return Type of Function Templates
Section titled “Return Type of Function Templates”Continuing the example of the add() function template, wouldn’t it be nice to let the compiler deduce the type of the return value? It would; however, the return type depends on the template type parameters, so how can you do this? For example, take the following function template:
template <typename T1, typename T2>RetType add(const T1& t1, const T2& t2) { return t1 + t2; }In this example, RetType should be the type of the expression t1+t2, but you don’t know this because you don’t know what T1 and T2 are.
As discussed in Chapter 1, since C++14 you can ask the compiler to automatically deduce the return type for a function. So you can simply write add() as follows:
template <typename T1, typename T2>auto add(const T1& t1, const T2& t2) { return t1 + t2; }However, using auto to deduce the type of an expression strips away reference and const qualifiers, while decltype does not strip those. This stripping is fine for the add() function template because operator+ usually returns a new object anyway, but this stripping might not be desirable for certain other function templates, so let’s see how you can avoid it.
Before continuing with the function template examples, however, let’s first look at the differences between auto and decltype using a non-template example. Suppose you have the following function:
const std::string message { "Test" };
const std::string& getString() { return message; }You can call getString() and store the result in a variable with the type specified as auto as follows:
auto s1 { getString() };Because auto strips reference and const qualifiers, s1 is of type string, and thus a copy is made. If you want a reference-to-const, you can explicitly make it a reference and mark it const as follows:
const auto& s2 { getString() };An alternative solution is to use decltype, which does not strip anything:
decltype(getString()) s3 { getString() };In this case, s3 is of type const string&; however, there is code duplication because you need to specify getString() twice, which can be cumbersome when getString() is a more complicated expression. This is solved with decltype(auto):
decltype(auto) s4 { getString() };s4 is also of type const string&.
So, with this knowledge, we can write our add() function template using decltype(auto) to avoid stripping any const and reference qualifiers:
template <typename T1, typename T2>decltype(auto) add(const T1& t1, const T2& t2) { return t1 + t2; }Before C++14—that is, before function return type deduction and decltype(auto) were supported—the problem was solved using decltype(expression), introduced with C++11. For example, you would think you could write the following:
template <typename T1, typename T2>decltype(t1+t2) add(const T1& t1, const T2& t2) { return t1 + t2; }However, this is wrong. You are using t1 and t2 in the beginning of the prototype line, but these are not yet known. t1 and t2 become known once the semantic analyzer reaches the end of the parameter list.
This problem used to be solved with the alternative function syntax. Note that with this syntax, auto is used at the beginning of the prototype line, and the actual return type is specified after the parameter list (trailing return type); thus, the names of the parameters (and their types, and consequently, the type t1+t2) are known:
template <typename T1, typename T2>auto add(const T1& t1, const T2& t2) -> decltype(t1+t2){ return t1 + t2;}Another option is to use std::declval<>(), which returns an rvalue reference to the type you requested. This is not a fully constructed object, as no constructor gets called! You cannot use the object at run time. You should only use it, for example, in combination with decltype(). It comes in handy in generic code and when you need to create an object of some unknown type. In that case, you can’t call any sensible constructor as you don’t know what constructors the unknown type supports. Let’s look at an example. The earlier add() code snippet with an explicit return type of decltype(t1+t2) at the beginning of the prototype line doesn’t compile because the names t1 and t2 are not yet known at that time. To remedy this, you can use declval<>() as follows:
template <typename T1, typename T2>decltype(std::declval<T1>() + std::declval<T2>()) add(const T1& t1, const T2& t2){ return t1 + t2;}Abbreviated Function Template Syntax
Section titled “Abbreviated Function Template Syntax”The abbreviated function template syntax makes writing function templates easier. Let’s take another look at the add() function template from the previous section:
template <typename T1, typename T2>decltype(auto) add(const T1& t1, const T2& t2) { return t1 + t2; }Looking at this, it’s a rather verbose syntax to specify a simple function template. With the abbreviated function template syntax, this can be written more elegantly as follows:
decltype(auto) add(const auto& t1, const auto& t2) { return t1 + t2; }With this syntax, there is no template header, template<>, anymore to specify template parameters. Instead, where previously the implementation used T1 and T2 as types for the parameters of the function, they are now specified as auto. This abbreviated syntax is just syntactical sugar; the compiler automatically translates this abbreviated implementation to the longer original code. Basically, every function parameter that is specified as auto becomes a template type parameter.
There are two caveats that you have to keep in mind. First, each parameter specified as auto becomes a different template type parameter. Suppose you have a function template like this:
template <typename T>decltype(auto) add(const T& t1, const T& t2) { return t1 + t2; }This version has only a single template type parameter, and both parameters to the function, t1 and t2, are of type const T&. For such a function template, you cannot use the abbreviated syntax, as that would be translated to a function template having two different template type parameters.
A second issue is that you cannot use the deduced types explicitly in the implementation of the function template, as these automatically deduced types have no name. If you need this, you either need to keep using the longer function template syntax or use decltype() to figure out the type.
VARIABLE TEMPLATES
Section titled “VARIABLE TEMPLATES”In addition to class templates, class member function templates, and function templates, C++ supports variable templates. The syntax is as follows:
template <typename T>constexpr T pi { T { 3.141592653589793238462643383279502884 } };This is a variable template for the value of π. To get the value of pi in a certain type, you use the following syntax:
float piFloat { pi<float> };auto piLongDouble { pi<long double> };You will always get the closest value of pi representable in the requested type. Just like other types of templates, variable templates can also be specialized.
CONCEPTS
Section titled “CONCEPTS”Concepts are named requirements used to constrain template arguments of class and function templates. These are written as predicates and evaluated at compile time to verify template arguments passed to a template. The main goal of concepts is to make template-related compiler errors more human readable. Everybody has encountered the situation where the compiler spews out hundreds or even thousands of lines of errors when you provide the wrong argument for a class or function template. It’s not always easy to dig through those compiler errors to find the root cause.
The reason why the compiler generates that many errors is that the compiler just blindly instantiates templates with the template arguments you provide. Once a template is instantiated, it is compiled, and only then is the compiler able to find out if the provided template type arguments do not support certain operations required deep down in the template implementation. This can be far away from the place where you instantiated a template, hence the myriad of errors. With concepts, the compiler can verify that provided template arguments satisfy certain constraints before it even starts instantiating a template.
Concepts allow the compiler to output readable error messages if certain type constraints are not satisfied. As such, to get meaningful semantical errors, it’s recommended to write concepts to model semantic requirements. Avoid concepts that validate only for syntactic aspects without any semantic meaning, such as a concept that just checks if a type supports operator+. Such a concept would check only for syntax, not semantics. An std::string supports operator+, but obviously, it has a completely different meaning compared to operator+ for integers. On the other hand, concepts such as sortable and swappable are good examples of concepts modeling some semantic meaning.
Let’s start by looking at the syntax to write concepts.
Syntax
Section titled “Syntax”The syntax of a concept definition, a template for a named set of constraints, is as follows:
template <parameter-list>concept concept-name = constraints-expression;It starts with a familiar template header, template<>, but unlike class and function templates, concepts are never instantiated. Next, a new keyword, concept, is used followed by the name of the concept. You can use any name you want. The constraints-expression can be any constant expression, that is, any expression that can be evaluated at compile time. The constraints expression must result in a Boolean value (exactly bool as the compiler will not insert any type conversions). This can also be a conjunction, &&, or disjunction, ||, of constant expressions. The constraints are never evaluated at run time. Constraints expressions are discussed in detail in the next section.
A concept expression has the following syntax:
concept-name<argument-list>Concept expressions evaluate to either true or false. If it evaluates to true, then it is said that the given template arguments model the concept. The next section gives an example.
Constraints Expression
Section titled “Constraints Expression”Constant expressions that evaluate to a Boolean can directly be used as constraints for a concept definition. It must evaluate exactly to a Boolean without any type conversions. Here’s an example:
template <typename T>concept Big = sizeof(T)> 4;Based on this concept, the concept expressions Big<char> and Big<short> usually evaluate to false, while concept expressions like Big<long double> will usually evaluate to true. A concept expression evaluates to a Boolean value at compile time that can be verified using a static assertion. A static assertion uses static_assert() and 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. Chapter 26 discusses static assertions in a bit more detail, but their use with concept expressions is straightforward. The following code asserts that Big<char> and Big<short> indeed evaluate to false, and that Big<long double> evaluates to true:
static_assert(!Big<char>);static_assert(!Big<short>);static_assert(Big<long double>);When compiling this, there shouldn’t be any compilation errors. However, if you remove the exclamation point in the first line, then the compiler will issue an error similar to the following:
error C2607: static assertion failed01_Big.cpp(4,15): message : the concept 'Big<char>' evaluated to false01_Big.cpp(2,25): message : the constraint was not satisfiedTogether with the introduction of concepts, a new type of constant expression is introduced called a requires expression, used to define the syntactical requirements of concepts, and explained next.
Requires Expressions
Section titled “Requires Expressions”A requires expression has the following syntax:
requires (parameter-list) { requirements; }The (parameter-list) is optional and is syntactically similar to the parameter list of functions, except that default argument values are not allowed. The parameter list of a requires expression is used to introduce named variables that are local to the body of the requires expression. The body of a requires expression cannot have regular variable declarations.
The requirements is a sequence of requirements. Each requirement must end with a semicolon.
There are four types of requirements: simple, type, compound, and nested requirements, all discussed in the upcoming sections.
Simple Requirements
Section titled “Simple Requirements”A simple requirement is an arbitrary expression statement, not starting with requires. Variable declarations, loops, conditional statements, and so on are not allowed. This expression statement is never evaluated; the compiler simply validates that it compiles.
For example, the following concept definition specifies that a type T must be incrementable; that is, type T must support the post- and prefix ++ operator. Remember, you cannot define local variables in the body of a requires expression; instead, you define those as parameters, x in this example.
template <typename T>concept Incrementable = requires(T x) { x++; ++x; };Type Requirements
Section titled “Type Requirements”A type requirement verifies that a certain type is valid. It starts with the keyword typename, followed by the type to check. For example, the following concept requires that a certain type T has a value_type member:
template <typename T>concept C = requires { typename T::value_type; };A type requirement can also be used to verify that a certain template can be instantiated with a given type. Here’s an example:
template <typename T>concept C = requires { typename SomeTemplate<T>; };Compound Requirements
Section titled “Compound Requirements”A compound requirement can be used to verify that something does not throw any exceptions and/or to verify that a certain function returns a certain type. The syntax is as follows:
{ expression } noexcept -> type-constraint;Both the noexcept and ->type-constraint are optional. There is no semicolon after expression inside the curly brackets, but there is a semicolon at the end of the compound requirement.
Let’s look at an example. The following concept requires that a given type has a non-throwing destructor and non-throwing swap() member function:
template <typename T>concept C = requires (T x, T y) { { x.˜T()} noexcept; { x.swap(y) } noexcept;};The type-constraint can be any type constraint. A type constraint is simply the name of a concept with zero or more template type arguments. The type of the expression on the left of the arrow is automatically passed as the first template type argument to the type constraint. Hence, a type constraint always has one less argument than the number of template type parameters of the corresponding concept definition. For example, a type constraint for a concept definition with a single template type does not require any template arguments; you can either specify empty brackets, <>, or omit them. This might sound tricky, but an example will make this clear. The following concept validates that a given type has a member function called size() returning a type that is convertible to a size_t. It also validates that size() is marked as const because the parameter x is of type const T.
template <typename T>concept C = requires (const T x) { { x.size() } -> convertible_to<size_t>;};std::convertible_to<From, To> is a concept predefined by the Standard Library in <concepts> and has two template type parameters. The type of the expression on the left of the arrow is automatically passed as the first template type argument to the convertible_to type constraint. As such, you only need to specify the To template type argument, size_t in this case.
Here is another example. The following concept requires that instances of a type T are comparable:
template <typename T>concept Comparable = requires(const T a, const T b) { { a == b } -> convertible_to<bool>; { a < b } -> convertible_to<bool>; // … similar for the other comparison operators …};Remember, the type-constraint in a compound requirement must be a type constraint, never a type. The following, for instance, will not compile:
Nested Requirements
Section titled “Nested Requirements”A requires expression can have nested requirements. For example, the following is a concept that requires a type to support the pre- and postfix increment and decrement operations. Additionally, the requires expression has a nested requirement to verify that the size of the type is 4 bytes.
template <typename T>concept C = requires (T t) { ++t; --t; t++; t--; requires sizeof(t) == 4;};Combining Concept Expressions
Section titled “Combining Concept Expressions”Existing concept expressions can be combined using conjunctions (&&) and disjunctions (||). For example, suppose you have a Decrementable concept, similar to Incrementable; the following example shows a concept that requires a type to be both incrementable and decrementable:
template <typename T>concept IncrementableAndDecrementable = Incrementable<T> && Decrementable<T>;Predefined Standard Concepts
Section titled “Predefined Standard Concepts”The Standard Library defines a whole collection of predefined concepts, more than 100 of them, divided into a number of categories. The following list gives just a few example concepts of each category, all defined in <concepts> and in the std namespace:
- Core language concepts:
same_as,derived_from,convertible_to,integral,floating_point,copy_constructible, and so on - Comparison concepts:
equality_comparable,totally_ordered, and so on - Object concepts:
movable,copyable, and so on - Callable concepts:
invocable,predicate, and so on
Additionally, <iterator> defines iterator-related concepts such as random_access_iterator, forward_iterator, incrementable, indirectly_copyable, indirectly_swappable, and so on. A concept such as indirectly_copyable is not meant to verify that a given iterator itself is copyable, but rather that the elements pointed to by a given iterator are copyable, hence the “indirectly” part of the name. Finally, <iterator> also defines algorithm requirements such as mergeable, sortable, permutable, and so on.
The C++ ranges library also provides a number of standard concepts. Chapter 17, “Understanding Iterators and the Ranges Library,” discusses iterators and ranges in detail, while Chapter 20 goes deeper into the algorithms provided by the Standard Library. Consult your favorite Standard Library reference for a full list of available standard concepts.
If any of these standard concepts is what you need, you can use them directly without having to implement your own. For example, the following concept requires that a type T is derived from class Foo:
template <typename T>concept IsDerivedFromFoo = derived_from<T, Foo>;The following concept requires that type T is convertible to bool:
template <typename T>concept IsConvertibleToBool = convertible_to<T, bool>;More concrete examples follow in the upcoming sections.
Of course, these standard concepts can also be combined into more specific concepts. The following concept, for example, requires a type T to be both default and copy constructible:
template <typename T>concept DefaultAndCopyConstructible = default_initializable<T> && copy_constructible<T>;Type-Constrained auto
Section titled “Type-Constrained auto”Type constraints can be used to constrain variables defined with auto type deduction, to constrain return types when using function return type deduction, to constrain parameters in abbreviated function templates and generic lambda expressions, and so on. Using type constraints with auto type deduction makes the code more self-documenting. It also results in better error messages if the constraints get violated at some point, as the error then points to the variable definition instead of to some unsupported operation later in the code.
For example, the following compiles without errors because the deduced type is int, which models the Incrementable concept:
Incrementable auto value1 { 1 };However, the following causes a compilation error stating that the constraints are not satisfied. The deduced type is std::string, due to the use of the standard literal s, and string does not model Incrementable:
Incrementable auto value { "abc"s };Type Constraints and Function Templates
Section titled “Type Constraints and Function Templates”There are several syntactically different ways to use type constraints with function templates. A first syntax is to use a requires clause as follows:
template <typename T> requires constraints-expressionvoid process(const T& t);The constraints-expression can be any constant expression, or a conjunction and disjunction of constant expressions, resulting in a Boolean type, just as the constraints-expression of a concept definition. For example, the constraints expression can be a concept expression:
template <typename T> requires Incrementable<T>void process(const T& t);or a predefined standard concept:
template <typename T> requires convertible_to<T, bool>void process(const T& t);or a requires expression (note the two requires keywords):
template <typename T> requires requires(T x) { x++; ++x; }void process(const T& t);or any constant expression resulting in a Boolean:
template <typename T> requires (sizeof(T) == 4)void process(const T& t);or a combination of conjunctions and disjunctions:
template <typename T> requires Incrementable<T> && Decrementable<T>void process(const T& t);or a type trait (see Chapter 26):
template <typename T> requires is_arithmetic_v<T>void process(const T& t);The requires clause can also be specified after the function header, called a trailing requires clause:
template <typename T>void process(const T& t) requires Incrementable<T>;Another syntax is to use the familiar template<> syntax, but instead of using typename (or class), you use a type constraint. Here are two examples:
template <convertible_to<bool> T>void process(const T& t);
template <Incrementable T>void process(const T& t);These are type constraints as discussed in the section on compound requirements, so they require one less template type parameter than usual. Concretely:
template <convertible_to<bool> T>void process(const T& t);is entirely analogous to:
template <typename T> requires convertible_to<T, bool>void process(const T& t);Yet another, more elegant, syntax to use type constraints combines the abbreviated function template syntax, discussed earlier in this chapter, and type constraints, resulting in the following nice and compact syntax. Mind you, even though there is no template header, template<>, don’t be fooled: process() is still a function template.
void process(const Incrementable auto& t);Compilation errors when requirements are violated are pretty readable. Calling process() with an integer argument works as expected. Calling it with an std::string, for instance, results in an error complaining about unsatisfied constraints. As an example, the Clang compiler produces the following errors. On first sight, it might still look a bit verbose, but it’s surprisingly readable.
<source>:17:2: error: no matching function for call to 'process' process(str); ^˜˜˜˜˜˜<source>:9:6: note: candidate template ignored: constraints not satisfied [with T = std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>]void process(const T& t) ^<source>:8:11: note: because 'std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>' does not satisfy 'Incrementable'template <Incrementable T> ^<source>:6:42: note: because 'x++' would be invalid: cannot increment value of type 'std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>'concept Incrementable = requires(T x) { x++; ++x; };You are free to use whatever syntax suits you best, but in certain cases, you don’t have a choice but to use the trailing requires clause syntax:
- When the constraint uses parameter names of the function, the trailing requires clause syntax must be used; otherwise, the function template’s parameter names are not yet in scope.
- To constrain a member function of a class template that is defined directly in the class template’s body, the trailing requires clause syntax is required because such a member function doesn’t have a template header.
Constraint Subsumption
Section titled “Constraint Subsumption”You can overload a function template with different type constraints. The compiler always uses the template with the most specific constraints; the more specific constraints subsume/imply the lesser constraints. Here’s an example:
template <typename T> requires integral<T>void process(const T& t) { println("integral<T>"); }
template <typename T> requires (integral<T> && sizeof(T) == 4)void process(const T& t) { println("integral<T> && sizeof(T) == 4"); }Suppose you have the following calls to process():
process(int { 1 });process(short { 2 });Then the output is as follows on a typical system where an int has 32 bits and a short has 16 bits:
integral<T> && sizeof(T) == 4integral<T>The compiler resolves any subsumption by first normalizing the constraints expressions. During normalization of a constraints expression, all concept expressions are recursively expanded to their definitions until the result is a single constant expression consisting of conjunctions and disjunctions of constant Boolean expressions. A normalized constraints expression then subsumes another one if the compiler can prove that it implies the other one. Only conjunctions and disjunctions are taken into account to prove any subsumption, never negations.
This subsumption reasoning is done only at the syntactical level, not semantically. For example, sizeof(T)>4 is semantically more specific than sizeof(T)>=4, but syntactically the former will not subsume the latter.
One caveat, though, is that type traits, such as the std::is_arithmetic trait used earlier, are not expanded during normalization. Hence, if there is a predefined concept and a type trait available, you should use the concept and not the type trait. For example, use the std::integral concept instead of the std::is_integral type trait.
Type Constraints and Class Templates
Section titled “Type Constraints and Class Templates”All type constraints examples up to now are using function templates. Type constraints can also be used with class templates, using a similar syntax. As an example, let’s revisit the GameBoard class template from earlier in this chapter. The following is a new definition for it, requiring its template type parameter to be a derived class of GamePiece:
template <std::derived_from<GamePiece> T>class GameBoard : public Grid<T>{ public: // Inherit constructors from Grid<T>. using Grid<T>::Grid;
void move(std::size_t xSrc, std::size_t ySrc, std::size_t xDest, std::size_t yDest);};The member function implementations need to be updated as well. Here’s an example:
template <std::derived_from<GamePiece> T>void GameBoard<T>::move(std::size_t xSrc, std::size_t ySrc, std::size_t xDest, std::size_t yDest) { /*…*/ }Alternatively, you can also use a requires clause as follows:
template <typename T> requires std::derived_from<T, GamePiece>class GameBoard : public Grid<T> { /*…*/ };Type Constraints and Class Member Functions
Section titled “Type Constraints and Class Member Functions”It’s possible to put additional constraints on specific member functions of a class template. For example, the move() member function of the GameBoard class template could be further constrained to require that type T is moveable:
template <std::derived_from<GamePiece> T>class GameBoard : public Grid<T>{ public: // Inherit constructors from Grid<T>. using Grid<T>::Grid;
void move(std::size_t xSrc, std::size_t ySrc, std::size_t xDest, std::size_t yDest) requires std::movable<T>;};Such a requires clause also needs to be repeated on the member function definition:
template <std::derived_from<GamePiece> T>void GameBoard<T>::move(std::size_t xSrc, std::size_t ySrc, std::size_t xDest, std::size_t yDest) requires std::movable<T>{ /*…*/ }Remember that, thanks to selective instantiation discussed earlier in this chapter, you can still use this GameBoard class template with non-movable types, as long as you never call move() on it.
Constraint-Based Class Template Specialization and Function Template Overloading
Section titled “Constraint-Based Class Template Specialization and Function Template Overloading”As described earlier in this chapter, you can write specializations for class templates and overloads for function templates to have a different implementation for certain types. It’s also possible to write a specialization or overload for a collection of types satisfying certain constraints.
Let’s take one more look at the Find() function template from earlier in this chapter. To refresh your memory:
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.}This implementation uses the == operator to compare values. It’s usually not advisable to compare floating-point types for equality using ==, but instead to use an epsilon test. The following overload of Find() for floating-point types uses an epsilon test implemented in an AreEqual() helper function instead of operator==:
template <std::floating_point T>optional<size_t> Find(const T& value, const T* arr, size_t size){ for (size_t i { 0 }; i < size; ++i) { if (AreEqual(arr[i], value)) { return i; // Found it; return the index. } } return {}; // Failed to find it; return empty optional.}AreEqual() is defined as follows, also using a type constraint. A detailed discussion of the mathematics behind the epsilon test logic is outside the scope of this book and not important for this discussion.
template <std::floating_point T>bool AreEqual(T x, T y, int precision = 2){ // Scale the machine epsilon to the magnitude of the given values and // multiply by the required precision. return fabs(x - y) <= numeric_limits<T>::epsilon() * fabs(x + y) * precision || fabs(x - y) < numeric_limits<T>::min(); // The result is subnormal.}Best Practices
Section titled “Best Practices”As this section shows, concepts are a powerful mechanism to constrain types. They provide for a lot of flexibility. Always keep the following in mind:
- Prefer using predefined Standard Library concepts or combinations of them over writing your own, because writing your own complete and correct concepts is difficult and time-consuming.
- When you do write your own concepts, make sure they model semantic requirements, not just syntactical requirements. For example, if your code technically only requires
operator==and<, don’t write a concept that only requires the availability of those two operators, because that would be a syntactical constraint. Instead, require the type to be orderable—that’s a semantic constraint. - By using proper semantic type requirements up front, you are less likely to have to add more requirements later. For example, if your class template is constrained with a concept that just requires
operator==and<, then it could be that you might have to add a requirement in the future foroperator>. In doing so, you’ll break existing code. If you would have used a proper concept from the start modeling orderability, you won’t be breaking existing code. - If a parameter of a requires expression is not meant to be modified, mark the parameter as
constto capture that requirement. - When writing new class or function templates, try to put proper type constraints on all template type parameters. Unconstrained template type parameters should be a thing of the past.
- Remember that you can use type constraints with
autotype deduction.
SUMMARY
Section titled “SUMMARY”This chapter started a discussion on using templates for generic programming. You saw the syntax on how to write templates and examples where templates are really useful. It explained how to write class templates, class member function templates, and how to use template parameters. It further discussed how to use class template specialization to write special implementations of a template where the template parameters are replaced with specific arguments.
You also learned about variable templates, function templates, and the elegant abbreviated function template syntax. The chapter finished with a discussion of concepts, allowing you to put constraints on template parameters.
Chapter 26 continues the discussion on templates with some more advanced features such as class template partial specializations, variadic templates, and metaprogramming.
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 12-1: Write a
KeyValuePairclass template with two template type parameters:KeyandValue. The class should have two private data members to store a key and a value. Provide a constructor accepting a key and a value, and add appropriate getters and setters. Test your class by creating a few instantiations in yourmain()function and try class template argument deduction. -
Exercise 12-2: The
KeyValuePairclass template from Exercise 12-1 supports all kind of data types for both its key and value template type parameters. For example, the following instantiates the class template withstd::stringas the type for both the key and the value:KeyValuePair<std::string, std::string> kv { "John Doe", "New York" };However, using
const char*as template type arguments results in data members of typeconst char*, which is not what we want.Write a class template specialization for
const char*keys and values that converts the given strings tostd::strings. -
Exercise 12-3: Take your solution from Exercise 12-1 and make the appropriate changes to only allow integer types as the type of the key and only floating-point types as the type of the value.
-
Exercise 12-4: Write a function template called
concat()with two template type parameters and two function parameterst1andt2. The function first convertst1andt2to a string and then returns the concatenation of those two strings. For this exercise, focus only on types for whichstd::to_string()is supported. Create and use a proper concept to make sure users of the function template don’t try to use it with unsupported types. Try to write your function template without using thetemplatekeyword. -
Exercise 12-5: The
concat()function template from Exercise 12-4 only works with types that are supported bystd::to_string(). In this exercise, modify your solution to make it also work with strings, and any combinations. -
Exercise 12-6: Take the original
Find()function template from earlier in this chapter and add an appropriate constraint on the typeT.