A feast of facts and tricks that only experienced programmers know.
The following content is my takeaways after reading part of the book C++ Templates: The Complete Guide, Second Edition¹.
Part I: The Basics
- Types of templates:
- class templates
- function templates
- alias templates (since C++ 11)
- variable templates (since C++ 14) - Three basic kinds of template parameters:
- type parameters
- non-type parameters
- template parameters - Template parameters with default arguments may be followed by template parameters without default arguments (section 1.4).
- Unlike non-template classes and aliases, class templates and alias templates cannot be declared or defined inside functions or block scopes.
- Two approaches to declare a friend function in a class template and define it afterwards (section 2.4):
- Declare it with different template parameters from the ones used for the class template, or
- Forward declare the class template and the function first and then declare an instantiated function as a friend in the class template definition. - Since C++ 17, a non-type template parameter can be auto and even decltype(auto).
- If two function templates only differ by a trailing parameter pack, the one without the trailing parameter pack is preferred.
- Use fold expressions and pointer-to-member operators to define a variadic template to get a descendent a binary tree node along a path (section 4.2):
struct Node {
int value = 0;
Node* left = nullptr;
Node* right = nullptr;
// `Args` is a template parameter pack.
// `args` is a function parameter pack.
template <typename … Args>
Node* get_descendent(Args… args) {
return (this->* … ->*args);
}
};
auto left = &Node::left;
auto right = &Node::right;
// node is of type Node*
auto* node1 = node -> get_descendent(left, right, right, left);
- Types that depend on template params (e.g. typename T::iterator) are not qualified for perfect forwarding (section 6.1).
- Use std::enable_if and std::void_t to SFINAE out a member function template so that it won’t override special member functions (section 6.2).
- For string literals and raw arrays:
- Call-by-value decays so they become pointers to the element type.
- Any form of call-by-reference does not decay so the arguments become references that still refer to arrays. - Even if a parameter is declared to be passed by value, the caller can use std::ref() and std::cref() to pass objects by reference.
- Use std::remove_reference_t or std::decay_t to ensure the return type is not a reference.
- Substitution is distinct from the on-demand instantiation and only uses the declaration (not the body) of the template.
- All members of a class template are substituted, but their definitions will only be instantiated on demand.
- Two-phase lookup (section 14.3.1):
- Template parsing: complete lookup for non-dependent names and ordinary lookup for unqualified dependent names.
- Template instantiation: ADL for unqualified dependent names and complete lookup for qualified dependent names. - Fully specialized function templates act like ordinary functions and need to inline when defined in header files outside classes or structures.
- Use std::invoke() to invoke a passed member function on a passed object (with additional arguments in the parameter pack).
- Surprisingly, std::remove_const_t<const int&> yields const int&.
Part III: Templates and Design
- Polymorphism implemented via inheritance is bounded and dynamic, while polymorphism implemented via templates is unbounded and static.
- In STL, the container-specific operations are factored out into the iterators’ functionality.
- Traits provide an avenue to configure concrete elements for generic computations, and allow us to define type functions even for fundamental types and types defined in closed libraries (unbounded polymorphism).
- A traits template, which holds one or more traits of its template parameters, should in principle be SFINAE-friendly, meaning it can only fail at substitution, not at the point of instantiation.
- Traits, including both property traits and policy traits, represent natural additional properties of the main template parameters, and they tend to depend tightly on the main parameters.
- Policies represent configurable behavior for generic functions and types, and are mostly orthogonal to the main parameters.
- Tag dispatching: create a traits class that provides the tag and overload the function (or function template) based on different tags, as an alternative to SFINAE for algorithm specialization. std::in_place_t is a tag class example.
- Empty base class optimization (EBCO): C++ does not allocate memory for an empty base class so long as it does not cause that base class object to be allocated to the same address as another object of the same type.
- It is sometimes desirable to use private inheritance instead of composition to exploit EBCO.
- CRTP: template the base class on the derived class so as to factor out implementations that depend on the derived class without introducing runtime overhead (of virtual functions).
- Mixin: instead of making customized classes inherit from a common base class, create a common class template inheriting from a template parameter pack that represents customized classes (e.g. mixed-in classes).
- CRTP-mixin: pass the base class as a template argument of the mixed-in classes that it inherits from, so that the mixed-in classes could use interface of the base class template.
- Use virtual inheritance to avoid the common base class being duplicated in multiple inheritance (section 21.4).
- Type erasure provides unbounded and dynamic polymorphism by using one type to represent a variety of other types without inheritance.
- How to implement std::function (chapter 22):
- Create a base class templated on the parameter and return value types.
- Create a derived class additionally templated on the specific callable type and store the pointer to the callable object in it.
- Create a factory function that generates an instance of the derived class referenced by a pointer of the base class type.