[Book] C++ Templates - The Complete Guide (Second Edition)
Last Update:
Word Count:
Read Time:
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 | |
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
5template<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
3max(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
9template<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
3std::complex<flat> c1, c2; //doesn't provide operator <
...
::max(c1, c2);
templates are compiled in two phase:
- Without instantiation at definition time, the template code itself is checked for correctness ignoring the template parameters:
- Syntax errors.
- Using unknown names.
- Static assertions that don’t depend on template parameters are checked.
- At instantiation time, the template code is checked(again) to ensure that all code is valid.
1 | |
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 | |
1 | |
There are three ways to handle such errors:
- Cast the arguments so that they both match:
1
max(static_cast<double>(4), 7.2); - Specify(or qualify) explicity the type of T to prevent the compiler from attempting type deduction:
1
max<double>(4, 7.2); - Specify that the parameters may have different types.
Type Deduction for Default Arguments1
2
3
4
5template<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
5template<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 | |
Template Parameters for Return Types
1 | |
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 | |
1 | |
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
5template<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
5template<typename T1, typename T2>
auto max(T1 a, T2 b) -> decltype(b < a ? a : b)
{
return b < a ? a : b;
}1
2template<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 | |
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
11template<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 | |
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 | |
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 | |
New, we can call max():1
2
3auto 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 | |
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
5template<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 | |
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
7template<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
7template<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
2template<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 | |
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
9template<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 | |
An instantiated class template’s type can be used just like any other type.1
2
3
4
5
6void 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
2Stack<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
12template<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
4template<>
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
4void 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();
}