08 Templates
Introduction
Templates are filled in at compile time. This allows the compiler to generate code for different types. Templates are not limited to types, they can also be used for values and functions.
Instantiation
Templates are implicitly instantiated only when they are used. The code is therefore only generated for the types that are used. Template instantiation is repeated for each compilation unit (the compiler instantiates each template for every unit). The linker will then remove duplicate code. Explicit instantiation can lower compile time (mostly only useful for big projects).
Templates are also called compile-time polymorphism.
Types of Templates
Function Templates
Syntax: template<TemplateParamList> FunctionDefinition
TemplateParamListis a comma-separated list of template parameters. Valid template parameters are:typename TypeIdentifierclass TypeIdentifiertemplate<TemplateParamList>Value
TypeIdentifieris a type name. Either a basic type or a class.FunctionDefinitionis the function definition with the template parameters.
Class Templates
Syntax: template<TemplateParamList> ClassDefinition
TemplateParamListis a comma-separated list of template parameters. Valid template parameters are:typename TypeIdentifierclass TypeIdentifiertemplate<TemplateParamList>Value
TypeIdentifieris a type name. Either a basic type or a class.ClassDefinitionis the class definition with the template parameters.
Member Templates
Function templates that are applied to instance methods of a class.
Template Specialization
Specialization is used to provide a different implementation for a specific type.
template<typename T>
T min(T a, T b) {
return a < b ? a : b;
}
// Specialization must follow the template
template<>
char min<char>(char a, char b) {
return tolower(a) < tolower(b) ? a : b;
}
Partial Specialization
Partial specialization is used to provide a different implementation for a specific type, but not all template parameters.
Instanciation of a partial specialization is done by pattern matching. The most specialized template found is used.
template<typename T, class C> class MyClass {};
template<class C> class MyClass<char, C> {};
Specialization vs Overloading
Overloaded functions are always a better match than the specialization. Specialization is only used if no overloaded function is a better match.
template<typename T> void foo(T x) { std::cout << "Generic" << std::endl; }
template<> void foo(int* x) { std::cout << "Specialization" << std::endl; }
template<typename T> void foo(T* x) { std::cout << "Overloaded" << std::endl; }
int main() {
int i = 5;
foo(i); // Generic
foo(&i); // Overloaded (even though specialization exists)
}
Alias Templates
Alias templates are used to define a type alias for a template.
Array<std::vector<std::pair<std::string>>, 50> myArray;
// Alias template
template<typename Key, typename Value, size_t N>
using AKV = Array<std::pair<Key, Value>, N>;
// Partial
template<typename T> using A50 = Array<T, 50>;
template<size_t N> using Aint = Array<int, N>;
// Full
using AV50 = Array<std::vector<uint64_t>, 50>;
using Aint50 = Aint<50>;
Template Templates
Template parameters can be templates themselves if they are declared as such.
// Only works for classes with two type parameters
template<template<typename, typename> class C, class T, class A>
std::ostream& operator<<(std::ostream& os, const C<T,A>& v) {
os << '[';
auto it = v.begin();
if (it != v.end()) os << *it++;
for (; it != v.end(); it++) os << ", " << *it;
os << ']';
return os;
}
Variadic Templates
Functions and classes that require a variable number of arguments of arbitrary types.
Uses parameter packs (...).
template<typename... Ts> class C {};
size_t types = sizeof...(Ts); // Number of parameters
template<typename... Ts> void foo(const Ts&... args) {
std::cout << sizeof...(args) << std::endl;
}
// first: anchor
template<typename T> void logging(const T& t) {
std::cout << "[0] " << t << std::endl;
}
template<typename First, typename... Rest>
void logging(const First& first, const Rest&... rest) {
std::cout << "[" << sizeof...(rest) << "] " << first << ", ";
logging(rest...); // Unpack parameter pack
}
Perfect Forwarding
Forwarding of arguments to another function.
template<typename T> void f(T&& x) {...}: Generic right-value reference (T&&) is called universal reference.
- x is a l-value reference (
T& x) if f is called with a l-value - x is a r-value reference (
T&& x) if f is called with a r-value
template<typename T> void g(T&& y) { f(y); }
- inside
g:T&& yis always a l-value reference fis called with a l-value reference- Problem:
fis called with a l-value reference, even ifgwas called with a r-value reference
Solution: template<typename T> void g(T&& y) { f(std::forward<T>(y)); }
std::move(x) converts l-value to r-value
std::forward<T>(x) converts l-value to l-value and r-value to r-value
std::forward<T>
Usage
Used for universal references. Forwards l-value references as l-value references and r-value references as r-value references.
Implementation
std::forward<T>is converted tostd::moveif a r-value reference is passedstd::forward<T>has no effect if a l-value reference is passed
Sample
Simplified implementation of std::make_unique:
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
Would also apply to the logging implementation from above.
std::cout << "[" << sizeof...(std::forward<Rest>(rest)) << "] " << std::forward<First>(first) << ", ";
Dependant Typenames
Dependant types are member types that depend on a template parameter.
They are accessed with the typename keyword.
template<typename T>
struct Extrema {
using type = typename T::value_type; // typename required to tell the compiler that value_type is a type
type m_min, m_max;
Extrema(const T& t)
: m_min(*min_element(begin(t), end(t)))
, m_max(*max_element(begin(t), end(t))) {}
};
Extrema<std::vector<int>> x({8,3,5,6,1,3});
The same result can be achieved with template templates, but this solution is more flexible.
Metaprogramming
SFINAE
SFINAE (Substitution Failure Is Not An Error) is used to remove functions from overload resolution.
Substitution is tried as long as there are options for substitution (pattern matching). If substitution fails, the function is removed from overload resolution. Compilation will never fail because of substitution failure.
Example
struct Y {using type = int;};
template<typename T>
void func(typename T::type) { cout << "func(T::type)" << endl; }
template<typename T>
void func(T) { cout << "func(T)" << endl; }
func<Y>(1); // func(T::type), because Y has a member type "type"
func(1); // func(T), because int does not have a member type "type"
Concepts
Concepts are used to constrain template parameters. They are used to restrict the types that can be used as template parameters.
template<typename T, typename U> struct is_same : integral_constant<bool, false> {};
template<typename T> struct is_same<T, T> : integral_constant<bool, true> {};
template<typename T, typename U> constexpr bool is_same_v = is_same<T, U>::value;
template<class T, class U> concept Same = is_same_v<T, U> && is_same_v<U, T>;
template<typename T> requires Same<T, int>
void foo(T t) { cout << "Overloaded" << endl; }
template<typename T>
void foo(T t) requires Same<T, int> { cout << "Overloaded" << endl; }
template<Same<int> T> // implicit: Same<T, int>
void foo(T t) { cout << "Overloaded" << endl; }
Combining Conditions
template<typename T> requires
std::integral<T> ||
(std::invocable<T> && std::integral<typename std::invoke_result<T>::type>)
void function3(const T& x) {
if constexpr (std::invocable<T>) { // compiler decides which branch to compile
std::cout << "Result of call is " << x() << "\n"; // (): function operator
} else {
std::cout << "Value is " << x << "\n";
}
}
Defining Concepts
Syntax:
template<typename T> concept ConceptName = ConstraintExpression;
ConceptNameis the name of the conceptConstraintExpressionis a constraint expression. Valid constraint expressions are:- logical
constexprexpression requiresexpression- other concepts
- logical