C++ Templates
- Description: A note on the C++ templates, variadic templates, template metaprogramming, SFINAE, type traits, and concepts (C++20)
- My Notion Note ID: K2A-B1-17
- Created: 2018-10-27
- Updated: 2026-02-28
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. Templates
- 2. Template Metaprogramming
- 3. Concepts (C++20)
- 4. CRTP (Curiously Recurring Template Pattern)
1. Templates
- Templates = generic programming. Write type-independent code; compiler instantiates per type at compile time.
1.1 Default Template Arguments
- Template params can have defaults, like function params. Callers can omit.
template <typename T = char, typename Allocator = std::allocator<T>>
class Container {
// ...
};
Container<> c1; // T = char, Allocator = std::allocator<char>
Container<int> c2; // T = int, Allocator = std::allocator<int>
- C++11 added defaults for function templates:
template <typename T = int>
T identity(T value) {
return value;
}
auto x = identity(42); // T deduced as int
auto y = identity<double>(42); // T explicitly set to double
1.2 Template Template Parameters
- Template param that is itself a template. Pass a template (not an instantiation) as argument.
#include <vector>
#include <list>
#include <iostream>
template <template <typename, typename> class Container, typename Type>
class Wrapper {
Container<Type, std::allocator<Type>> data_;
public:
void add(const Type& item) { data_.push_back(item); }
size_t size() const { return data_.size(); }
};
int main() {
Wrapper<std::vector, int> v;
v.add(1);
v.add(2);
Wrapper<std::list, std::string> l;
l.add("hello");
return 0;
}
- Note: pre-C++17 required
classkeyword (nottypename). C++17+ allows both:
// C++14 and earlier: must use 'class'
template <template <typename, typename> class Container>
struct A {};
// C++17 and later: 'typename' is also allowed
template <template <typename, typename> typename Container>
struct B {};
1.3 Variadic Templates (C++11)
- Variable number of template args.
...operator has 2 roles:
- Left of a parameter name: declares parameter pack (binds 0+ args).
- Right of an expression: unpacks pack into separate args.
#include <iostream>
// Base case: no arguments
void print() {
std::cout << std::endl;
}
// Recursive case: peel off the first argument and recurse
template <typename T, typename... Args>
void print(const T& first, const Args&... rest) {
std::cout << first;
if constexpr (sizeof...(rest) > 0) {
std::cout << ", ";
}
print(rest...); // Unpack the remaining arguments
}
int main() {
print(1, "hello", 3.14, 'x');
// Output: 1, hello, 3.14, x
return 0;
}
- C++17 — fold expressions for concise syntax:
template <typename... Args>
auto sum(Args... args) {
return (args + ...); // Unary right fold
}
auto result = sum(1, 2, 3, 4); // 10
1.4 Static Variables in Templates
- Each instantiation with different type → its own static vars. Same type → shared.
#include <iostream>
template <typename T>
class Counter {
public:
static int count;
Counter() { ++count; }
};
template <typename T>
int Counter<T>::count = 0;
int main() {
Counter<int> a, b, c;
Counter<double> d, e;
std::cout << Counter<int>::count << std::endl; // 3
std::cout << Counter<double>::count << std::endl; // 2
// Counter<int> and Counter<double> have separate 'count' variables
return 0;
}
Counter<int>andCounter<double>are completely separate compiler-generated classes.
2. Template Metaprogramming
- TMP — compute at compile time via templates. Modern C++ (
constexpr,if constexpr, concepts) reduced need, but still valuable for library + legacy code.
2.1 Compile-Time Computation
- Compute via recursive specialization. Compiler evaluates → constant.
#include <iostream>
// General case: 2^n = 2 * 2^(n-1)
template <int N>
struct PowerOfTwo {
enum { value = 2 * PowerOfTwo<N - 1>::value };
};
// Base case: 2^0 = 1
template <>
struct PowerOfTwo<0> {
enum { value = 1 };
};
int main() {
// Computed entirely at compile time
std::cout << PowerOfTwo<8>::value << std::endl; // 256
return 0;
}
- Modern alternative: C++11+ —
constexprfunctions:
constexpr int powerOfTwo(int n) {
return n == 0 ? 1 : 2 * powerOfTwo(n - 1);
}
static_assert(powerOfTwo(8) == 256);
2.2 SFINAE (Substitution Failure Is Not An Error)
- Core TMP principle: template arg substitution failure → silently remove overload from candidate set, try others. Not an error.
- Enables conditionally-available overloads based on type properties.
#include <type_traits>
#include <iostream>
// Only enabled for integral types
template <typename T>
typename std::enable_if<std::is_integral<T>::value, void>::type
process(T value) {
std::cout << "Integer: " << value << std::endl;
}
// Only enabled for floating-point types
template <typename T>
typename std::enable_if<std::is_floating_point<T>::value, void>::type
process(T value) {
std::cout << "Float: " << value << std::endl;
}
int main() {
process(42); // "Integer: 42"
process(3.14); // "Float: 3.14"
// process("hello"); // Compile error: no matching overload
return 0;
}
2.3 std::enable_if
- Primary SFINAE tool. Conditionally defines a type member based on boolean condition.
// Definition (simplified):
template <bool Condition, typename T = void>
struct enable_if {}; // No 'type' member when Condition is false
template <typename T>
struct enable_if<true, T> {
using type = T; // 'type' exists only when Condition is true
};
Common patterns:
#include <type_traits>
// As return type (C++14 shorthand with _t and _v suffixes)
template <typename T>
std::enable_if_t<std::is_integral_v<T>, T>
doubleIt(T value) {
return value * 2;
}
// As template parameter (often cleaner)
template <typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
T tripleIt(T value) {
return value * 3;
}
- Modern alternative (C++20): concepts, much cleaner:
template <std::integral T>
T quadrupleIt(T value) {
return value * 4;
}
2.4 Type Traits
<type_traits>(C++11) — compile-time type inspection + transformation. Building blocks of SFINAE + TMP.
Common queries:
#include <type_traits>
static_assert(std::is_integral_v<int>); // true
static_assert(std::is_floating_point_v<double>); // true
static_assert(std::is_pointer_v<int*>); // true
static_assert(std::is_same_v<int, int>); // true
static_assert(std::is_base_of_v<Base, Derived>); // true (if Derived extends Base)
Transformations:
#include <type_traits>
// Remove const/volatile/reference qualifiers
using A = std::remove_const_t<const int>; // int
using B = std::remove_reference_t<int&>; // int
using C = std::remove_cv_t<const volatile int>; // int
// Add qualifiers
using D = std::add_const_t<int>; // const int
using E = std::add_pointer_t<int>; // int*
using F = std::add_lvalue_reference_t<int>; // int&
2.5 std::decay
std::decay— applies the implicit conversions of pass-by-value: remove refs, cv-quals; array → pointer; function → fn pointer.- Useful when storing a copy of an arg's type (e.g., in a thread or callback).
#include <type_traits>
// All of these decay to the same type:
static_assert(std::is_same_v<std::decay_t<int&>, int>);
static_assert(std::is_same_v<std::decay_t<const int&>, int>);
static_assert(std::is_same_v<std::decay_t<int&&>, int>);
static_assert(std::is_same_v<std::decay_t<int[10]>, int*>);
static_assert(std::is_same_v<std::decay_t<int(double)>, int(*)(double)>);
- Practical use:
std::threadusesdecayinternally to store copies of callable + args. Handles refs/arrays correctly.
2.6 std::result_of / std::invoke_result
- Deduce return type of calling a callable with specific arg types.
- Important:
std::result_ofdeprecated C++17, removed C++20. Usestd::invoke_result.
#include <type_traits>
#include <functional>
int fn(int) { return 0; }
using fn_ref = int(&)(int);
using fn_ptr = int(*)(int);
struct fn_class {
int operator()(int i) { return i; }
};
// C++17+ syntax (preferred):
using A = std::invoke_result_t<decltype(fn), int>; // int
using B = std::invoke_result_t<fn_ptr, int>; // int
using C = std::invoke_result_t<fn_class, int>; // int
// C++11/14 syntax (deprecated in C++17, removed in C++20):
// using D = std::result_of<decltype(fn)&(int)>::type; // int
3. Concepts (C++20)
- Concepts (C++20) — named compile-time predicates on template params.
- Replace SFINAE-based constraints with cleaner syntax + dramatically better errors.
3.1 The requires Clause
requiresclause attaches a compile-time predicate to a template:
#include <concepts>
template <typename T>
requires std::integral<T>
T add(T a, T b) { return a + b; }
// Or as a trailing requires:
template <typename T>
T add2(T a, T b) requires std::integral<T> { return a + b; }
// Or as the abbreviated form (replaces `typename`):
auto add3(std::integral auto a, std::integral auto b) { return a + b; }
- Predicate fails → fn removed from overload resolution. Like SFINAE, but diagnostic actually tells you what went wrong:
error: no matching function for call to 'add'
note: candidate disabled by failed constraint 'std::integral<T>'
requiresexpression (different from clause) — tests whether expressions are well-formed:
template <typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::same_as<T>; // a + b must compile AND return T
};
3.2 Defining a Concept
#include <concepts>
#include <type_traits>
#include <iterator>
template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
template <typename T>
concept Container = requires(T t) {
typename T::value_type; // must have a value_type
typename T::iterator; // must have an iterator type
{ t.begin() } -> std::input_iterator;
{ t.end() } -> std::input_iterator;
{ t.size() } -> std::convertible_to<std::size_t>;
};
template <Container C>
void print(const C& c) {
for (const auto& x : c) std::cout << x;
}
3.3 Library Concepts
- stdlib provides many in
<concepts>:
| Concept | Tests |
|---|---|
same_as<T, U> |
Exact type equality |
convertible_to<T, U> |
T converts to U |
derived_from<Derived, Base> |
Inheritance |
integral<T> |
Integer types |
floating_point<T> |
Float types |
signed_integral, unsigned_integral |
Signed-ness |
equality_comparable<T> |
==, != defined |
totally_ordered<T> |
All ordering operators defined |
copy_constructible<T>, move_constructible<T> |
Construction |
default_initializable<T> |
T{} works |
invocable<F, Args...> |
F can be called with Args |
predicate<F, Args...> |
F returns bool-convertible |
<ranges>adds range concepts (std::ranges::range,view,sized_range, ...).<iterator>adds iterator concepts (input_iterator,forward_iterator, etc.).
3.4 Concepts vs SFINAE
// SFINAE (C++11): verbose, terrible error messages
template <typename T,
typename = std::enable_if_t<std::is_integral_v<T>>>
T double_it(T x) { return x * 2; }
// Concepts (C++20): clean, helpful errors
template <std::integral T>
T double_it(T x) { return x * 2; }
- Concepts combine naturally with
&&and||:
template <typename T>
requires std::integral<T> && (sizeof(T) >= 4)
T big_int_op(T x);
- C++20+: prefer concepts. SFINAE still relevant for backward compat + niche cases concepts can't express.
4. CRTP (Curiously Recurring Template Pattern)
- Common pattern — class derives from template instantiated with itself:
template <typename Derived>
class Base {
public:
void interface() {
static_cast<Derived*>(this)->implementation();
}
};
class MyClass : public Base<MyClass> {
public:
void implementation() { /* ... */ }
};
- Looks recursive but isn't —
Base<MyClass>is a complete type by the timeMyClass's body is parsed.
Why CRTP
- Static polymorphism — dispatch resolved at compile time, no vtable.
Use cases:
- Inject behavior into a class without runtime cost. Base calls methods derived implements.
- Mixins — add capabilities (comparison, hashing, cloning) via inheritance from a CRTP helper.
- Counting / registry —
template <typename T> struct Counted { static int count; ... };gives eachTits own counter. - Expression templates — lazy expression trees (Eigen).
Example: comparison mixin
template <typename Derived>
struct Comparable {
friend bool operator!=(const Derived& a, const Derived& b) { return !(a == b); }
friend bool operator< (const Derived& a, const Derived& b) {
return a.compare(b) < 0;
}
friend bool operator> (const Derived& a, const Derived& b) { return b < a; }
friend bool operator<=(const Derived& a, const Derived& b) { return !(b < a); }
friend bool operator>=(const Derived& a, const Derived& b) { return !(a < b); }
};
class Version : public Comparable<Version> {
public:
int compare(const Version& o) const; // implement once
bool operator==(const Version& o) const;
};
// All other comparison operators come for free, with no runtime cost
CRTP vs virtual
| CRTP | Virtual | |
|---|---|---|
| Dispatch | Compile-time | Runtime |
| Cost | Zero (inlinable) | One indirect call per virtual function |
| Heterogeneous containers | No (vector<Base<T>> is per-T) |
Yes (vector<Base*>) |
| Use case | Performance-critical, known type | Heterogeneous polymorphism |
- Modern C++: concepts (C++20, § 3) often replace CRTP for static polymorphism — express requirements directly, not via inheritance.
- CRTP still useful for mixins (inheritance injecting members + operators).