[Book] C++ Templates - The Complete Guide (Second Edition)

First Post:

Last Update:

Word Count:
3.6k

Read Time:
22 min

El libro

Introduction

This article is used to keep notes and summaries of the book “C++ Templates - The Complete Guide (Second Edition)”.
The content will be continuously updated as I read through the book.

Reflection

Chapter.1 - Function Templates

1.1 - Function Templates

1
2
3
4
5
template<typename T>
T max(T a, T b)
{
return b < a ? a : b;
}

template parameters must be announced with syntax of the following form:

1
template<comma-separated-list of parameters>

Here, the type parameter is T. You can use any identifier as a parameter name, but using T is the convention. The type parameter represents an arbitrary type that is determined by the caller when the caller calls the function.

For historical reasons, you can also use the keyworkd class instead of typename to define a type parameter.

1
2
3
4
5
template<class T>
T max(T a, T b)
{
return b < a ? a : b;
}

Semantically there is no difference. However, because this use of class can be misleading(not only class types can be substituted for T), you should prefer the use of typename in this context.
The keyword struct cannot be used in place of typename when declaring type parameters.

Example of using max() function template:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <string>
#include "max1.hpp"

int main()
{
int i = 42;
std::cout << "max(7, i): " << ::max(7, i) << std::endl;

double f1 = 3.4;
double f2 = -6.7;
std::cout << "max(f1, f2): " << ::max(f1, f2) << std::endl;

std::string s1 = "mathematics";
std::string s2 = "math";
std::cout << "max(s1, s2): " << ::max(s1, s2) << std::endl;
}

Output:
1
2
3
max(7,i): 42
max(f1,f2): 3.4
max(s1,s2): mathematics

Note that each call of the max() template is qualified with ::. This is to ensure that our max() template is found in the global namespace. This is also a std::max() template in the standard libaray, which under some circumstances may be called or maylead to ambiguity.

void is a valid template argument provided the resulting code is valid:

1
2
3
4
5
6
7
8
9
template<typename T>
T foo(T*)
{

}

void *vp = nullptr;
foo(vp); //OK
//foo(void*); NOT OK

Two-Phase Translation

An attempt to instantiate a template for a type that doesn’t support all the operations used within it will result in a compile-time error:

1
2
3
std::complex<flat> c1, c2; //doesn't provide operator <
...
::max(c1, c2);

templates are compiled in two phase:

  1. Without instantiation at definition time, the template code itself is checked for correctness ignoring the template parameters:
    1. Syntax errors.
    2. Using unknown names.
    3. Static assertions that don’t depend on template parameters are checked.
  2. At instantiation time, the template code is checked(again) to ensure that all code is valid.
1
2
3
4
5
6
7
8
template<typename T>
void foo(T t)
{
undeclared(); // first-phase compile-time error if undeclared() unknown
undeclared(t); // second-phase compile-time error if undeclared(T) unknown
static_assert(sizeof(int) > 10, // always fails if sizeof(int)<=10 "int too small");
static_assert(sizeof(T) > 10, //fails if instantiated for T with size <=10 "T too small");
}

1.2 - Template Arugment Deduction

Automatic type conversions are limited during type deduction:

  • When declaring call parameters by reference, even trivial conversions do not apply to type deduction. Two arguments declared with the same template parameter T must match exactly.
  • When declaring call parameters by value, only trivial conversions that decay are supported: Qualifications with const or volatile are ignored, references convert to the referenced type, and raw arrays or functions convert to the corresponding pointer type.
1
2
3
4
5
6
7
8
9
10
template<typename T>
T max (T a, T b);

int const c = 42;
max(i, c); // OK: T is deduced as int
max(c, c); // OK: T is deduced as int
int& ir = i;
max(i, ir); // OK: T is deduced as int
int arr[4];
foo(&i, arr); // OK: T is deduced as int*
1
2
3
max(4, 7.2); //ERROR: T can be deduced as int or double
std::string s;
foo("Hello", s); //ERROR: T can be deduced as char const[6] or std::string

There are three ways to handle such errors:

  1. Cast the arguments so that they both match:
    1
    max(static_cast<double>(4), 7.2);
  2. Specify(or qualify) explicity the type of T to prevent the compiler from attempting type deduction:
    1
    max<double>(4, 7.2);
  3. Specify that the parameters may have different types.

Type Deduction for Default Arguments

1
2
3
4
5
template<typename T>
void f(T = "");

f(1); //OK: deduced T to be int, so that it calls f<int>(1)
f(); //ERROR: cannot deduce T

To support this case:
1
2
3
4
5
template<typename T = std::string>
void f(T = "");
...

f(); //OK

Important: Template argument deduction is performed before default arguments are considered.

1.3 - Multiple Template Parameters

1
2
3
4
5
6
7
template<typename T1, typename T2>
T1 max (T1 a, T2 b)
{
return b < a ? a : b;
}

auto m = ::max(4, 7.2); // OK, but type of first argument defines return type

Template Parameters for Return Types

1
2
template<typename T1, typename T2, typename RT>
RT max (T1 a, T2 b);

However, template argument deduction does not take return types into account, and RT does not appear in the types of the function call parameters. Therefore, RT cannot be deduced.


1
2
3
4
template<typename T1, typename T2, typename RT>
RT max (T1 a, T2 b);

::max<int,double,double>(4, 7.2); // OK, but tedious
1
2
3
4
template<typename RT, typename T1, typename T2>
RT max (T1 a, T2 b);

::max<double>(4, 7.2) //OK: return type is double, T1 and T2 are deduced

Deducing the Return Type

Since C++14, this is possible by simply not declaring any return type(you still have to declare the return type to be auto):

1
2
3
4
5
template<typename T1, typename T2>
auto max(T1 a, T2 b)
{
return b < a ? a : b;
}

Before C++14, it is only possible to let the compiler determine the return type by more or less making the implementation of the function part of its declaration. In C++11 we can benefit from the fact that the trailing return type syntax allows us to use the call parameters. That is, we can declare that the return type is derived from what operator?: yields:

1
2
3
4
5
template<typename T1, typename T2>
auto max(T1 a, T2 b) -> decltype(b < a ? a : b)
{
return b < a ? a : b;
}

1
2
template<typename T1, typename T2>
auto max(T1 a, T2 b) -> decltype(b<a?:a:b);

1
2
3
4
5
6
#include <type_traits>
template<typename T1, typename T2>
auto max(T1 a, T2 b) -> typename std::decay<decltype(true ? a : b)>::type
{
return b < a ? a : b;
}

1.4 - Default Template Arguments

You can also define values for template parameters. These values are called default template arguments and can be used with any kind of template.

1
2
3
4
5
6
#include <type_traits>
template<typename T1, typename T2, typename RT = std::decay_t<decltype(trye ? T1() : T2())>>
RT max(T1 a, T2 b)
{
return b < a ? a : b;
}

Note again the usage of std::decay_t<> to ensure that no reference cna be returned.

Note also that this implementation requires that we are able to call default constructors for the passed types. There is another solution, using std::declval, which, however, make the declaration even more complicated(Section 11.2.3)

We can also use the std::common_type<> type trait to specify the default value for the return type:

1
2
3
4
5
6
7
8
9
10
#include <type_traits>

template<typename T1, typename T2, typename RT = std::common_type_t<T1, T2>>
RT max(T1 a, T2 b)
{
return b < a ? a : b;
}

auto a = ::max(4, 7.2);
auto b = ::max<double, int, long double>(7.2, 4);

However, again we have the problem that we have to specify three types to be able to specify the return type only.

We would need the ability to have the return type as the first template parameter, while still begin able to deduce it from the argument types.

It is possible to have default arguments for leading function template parameters even if parameters without default arguments follow:

1
2
3
4
5
6
7
8
9
10
11
template<typename RT = long, typename T1, typename T2>
RT max(T1 a, T2 b)
{
return b < a ? a : b;
}

int i;
long l;
...
max(i, l); //returns long(default argument of template parameter for returns type).
max<int>(4, 42); //returns int as explicitly requested

1.5 - Overloading Function Templates

Like ordinary functions, function templates can be overloaded. That is, you can have different function difinitions with the same function name so that when that name is used in a function call, a C++ compiler must decide which one of the various candidates to call.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//maximum of two int values:
int max(int a, int b)
{
return b < a ? a : b;
}

//maximum of two values of any type:
template<typename T>
T max(T a, T b)
{
return b < a ? a : b;
}

int main()
{
::max(7, 42); // calls the nontemplate for two ints
::max(7.0, 42.0); // calls max<double> (by argument deduction)
::max('a', 'b'); // calls max<char> (by argument deduction)
::max<>(7, 42); // calls max<int> (by argument deduction)
::max<double>(7, 42); // calls max<double> (no argument deduction)
::max('a', 42.7); // calls the nontemplate for two ints
}

If the template can generate a function with a better match, however, then the template is selected.

1
2
::max(7.0, 42.0); // calls the max<double> (by argument deduction)
::max('a', 'b'); // calls the max<char> (by argument deduction)

Here, the template is a better match because no conversion from double or char to int is required.

1
::max('a', 42.7);

Because automatic type conversion is not considered for deduced template parameters but is considered for ordinary function parameters, the last call uses the nontemplate function(while a and 42.7 both converted to int).


1
2
3
4
5
6
7
8
9
10
template<typename T1, typename T2>
auto max (T1 a, T2 b)
{
return b < a ? a : b;
}
template<typename RT, typename T1, typename T2>
RT max (T1 a, T2 b)
{
return b < a ? a : b;
}

New, we can call max():

1
2
3
auto a = ::max(4, 7.2); //uses first template.
auto b = ::max<long double>(7.2, 4); //uses second template, since the number of parameters is not matched for first template.
auto c = ::max<int>(4, 7.2); //ERROR: both function templates match.


A useful example would be to overload the maximum template for pointers and ordinary C-strings:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <cstring>
#include <string>

//maximum of two values of any type:
template<typename T>
T max(T a, T b)
{
return b < a ? a : bl
}

//maximum of two pointers:
template<typename T>
T* max(T *a, T *b)
{
return *b < *a ? a : b;
}

//maximum of two C-strings:
char const* max(char const* a, char const* b)
{
return std::strcmp(b, a) < 0 ? a : b;
}

int main()
{
int a = 7;
int b = 42;
auto m1 = ::max(a,b); // max() for two values of type int
std::string s1 = "hey";
std::string s2 = "you";
auto m2 = ::max(s1,s2); // max() for two values of type std::string
int* p1 = &b;
int* p2 = &a;
auto m3 = ::max(p1,p2); // max() for two pointers
char const* x = "hello";
char const* y = "world";
auto m4 = ::max(x,y); // max() for two C-strings
}

Note that in all overloads of max() we pass the arguments by value. In general, it is good idea not to change more than necessary when overloading function templates. You should limit your changes to the number of parameters or to specifying template parameters explicitly. Otherwise, unexpected effects may happen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>

template<typename T>
T max(T a, T b)
{
std::cout << "max<>()" << endl;
return b < a ? a : b;
}

// maximum of three values of any type:
template<typename T>
T max (T a, T b, T c)
{
return max (max(a,b), c); // uses the template version even for ints
} // because the following declaration comes
// too late:

// maximum of two int values:
int max (int a, int b)
{
std::cout << "max(int,int) \n";
return b < a ? a : b;
}
int main()
{
::max(47,11,33); // OOPS: uses max<T>() instead of max(int,int)
}

1.6 - But, Shouldn’t We…?

Pass by Value or by Reference?

Why not inline?

In general, function templates don’t have to be declared with inline. Unlike ordinary.
The only exception to this rule are full specializations of templates for specific types, so that the resulting code is no longer generic (all template parameters are defined).

Why not constexpr?

Since C++11, you can use constexpr to provide the ability to use code to compute some values at compile time. For a lost of templates this makes sense.

For example, to be able to use the maximum function at compile time, you have to declare it as follows:

1
2
3
4
5
template<typename T1, typename T2>
constexpr auto max(T1 a, T2 b)
{
return b < a ? a : b;
}

With this, you can use the maximum function template in places with compile-time context, such as when declaring the size of a raw array:
1
int a[::max(sizeof(char), 1000u)];

or the size of a std::array<>:
1
std::array<std::string, ::max(sizeof(char), 1000u)> arr;

Chapter.2 - Class Template

2.1 - Implementation of Class Template Stack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//stack1.hpp
#include <vector>
#include <cassert>

template<typename T>
class Stack {
private:
std::vecotr<T> elems; //elements

public:
void push(T const& elem); //push element
void pop(); //pop element

T const& top() const; //return top element
bool empty() const { //return whether the stack is empty.
return elems.empty();
}
}

template<typename T>
void Stack<T>::push (T const& elem)
{
elems.push_back(elem); //append copy of passed elem
}

template<typename T>
void Stack<T>::push (T const& elem)
{
elems.push_back(elem);
}

template<typename T>
void Stack<T>::pop()
{
assert(!elems.empty());
elems.pop_back();
}

template<typename T>
T const& Stack<T>::top() const
{
assert(!elems.empty());
return elems.back();
}

If, for example, you have to declare your own copy constructor and assignment operator, it typically looks like this:

1
2
3
4
5
6
7
template<typename T>
class Stack {
...
Stack (Stack const&); //copy constructor
Stack& operator= (Stack const&); //assignment operator
...
}

which is formally equivalent to:
1
2
3
4
5
6
7
template<typename T>
class Stack {
...
Stack (Stack<T> const&);
Stack<T>& operator= (Stack<T> const&);
...
}

but usually the <T> signals special handling of special template parameters, so it’s usually better to use the first form.

However, outside the class structure you’d need:

1
2
template<typename T>
bool operator== (Stack<T> const& lhs, Stack<T> const& rhs);

Note that in place where the name and not type of the class is required, only Stack may be used.

Implementation of Member Functions

To define a member function of a class template, you have o specify that it is a template, and you have to use the full type qualification of the class template.

1
2
3
4
5
template<typename T>
void Stack<T>::push(T const& elem)
{
elems.push_back(elem); //append copy of passed elem.
}

In this case, push_back() of the element vector is called, which appends the element at the end of the vector.

Note that pop_back() of vector removes the last elemtn but does not return it.
It is impossible to implement a completely exception-safe version of pop() that returns the removed. However, ignoring this danger, we could implement a pop() that returns the element just removed.

1
2
3
4
5
6
7
8
9
template<typename T>
T Stack<T>::pop()
{
assert(!elems.empty());
T elem = elems.back(); //save copy of last element.
elems.pop_back(); //remove last element.

return elem; //return copy of saved element.
}

2.2 - Use of Class Template Stack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//stack1.cpp
#include "stack1.hpp:
#include <iostream>
#include <string>

int main()
{
Stack<int> intStack;
Stack<std::string> stringStack;

//manipulate int stack
intStack.push(7);
std::cout << intStack.top() << '\n';

//manipulate string stack
stringStack.push("hello");
std::cout << stringStack.top() << '\n';
stringStack.pop();
}

An instantiated class template’s type can be used just like any other type.

1
2
3
4
5
6
void foo(Stack<int> const& s)   //parameter `s` is `int` stack.
{
using IntStack = Stack<int>; //IntStack is another name for `Stack<int>`.
Stack<int> istack[10]; //`istack` is an array of 10 `int` stacks.
IntStack istack2[10]; //`istack2` is also an array of 10 `int` stacks (same type).
}


Template arguments may be any type, such as pointers to float or even stack of int:

1
2
Stack<float*> flatPtrStack;       //stack of `float` pointers.
Stack<Stack<int>> intStackStack; //stack of stack of ints.

Note that before C++11 you had to put whitespace between the two closing template brackets:

1
Stack<Stack<int> > intStackStack; // OK with all C++ versions

If you didn’t do this, you were using operator >>, which resulted in a syntax error:
1
Stack<Stack<int>> intStackStack; // ERROR before C++11

The reason for the old behavior was that it helped the first pass of a C++ compiler to tokenize the
source code independent of the semantics of the code

2.3 - Partial Usage of Class Templates

Instead of print the stack contents with printOn it is better to implement operator<< for the stack. However, as usual operator<< has to be implemented as nonmember function, which then could call printOn() inline:

1
2
3
4
5
6
7
8
9
10
11
12
template<typename T>
class Stack {
...
void printOn() (std::ostream& strm) const {
...
}

friend std::ostream& operator<< (std::ostream& strm, Stack<T> const& s) {
s.printOn(strm);
return strm;
}
};

2.4 - Friends

2.5 - Specializations of Class Templates

Specializing class templates allows you to optimize
implementations for certain types or to fix a misbehavior of certain types for an instantiation of the class template.

If you specialize a class template, you must also specialize all member functions. Although it is possible to specialize a single member function of a class template, once you have done so, you can no longer specialize the whole class template instance that the specialized member belongs to.

To specialize a class template, you have to declare the class with a leading template<> and a specification of the types for which the class template is specialized. The types are used as a template argument and must be specified directly following the name of the class:

1
2
3
4
template<>
class Stack<std::string> {
...
};

For these specializations, any definition of a member function must be defined as an “ordinary” member function, which each occurence of T begin replaced by the specialized type:

1
2
3
4
void Stack<std::string>::push (std::string const& elem)
{
elems.push_back(elem);
}

2.6 - Partial Specialization

Class templates can be partially specialized. You can provide special implementations for particular circumstances, but some template parameters must still be defined by the user.

For example, we can define a special implementation of class `Stack> for pointers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include "stack1.hpp"

template<typename T>
class Stack<T*> {
private:
std::vector<T*> elems;

public:
void push(T*);
T* pop();
T* top() const;

bool empty() const {
return elems.empty();
}
}

template<typename T>
void Stack<T*>::push (T* elem)
{
elems.push_back(elem);
}

template<typename T>
T* Stack<T*>::pop()
{
assert(!elems.empty());
T* p = elems.back();
elems.pop_back();
return p;
}

template<typename T>
T* Stack<T*>::top() const
{
assert(!elems.empty());
return elems.back();
}

2.7 - Default Class Template Arguments

2.8 - Type Aliases

2.9 - Class Template Argument Deduction

2.10 - Templatized Aggregates

Chapter.3 - Nontype Template Parameters