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

  • 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 class keyword (not typename). 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:
  1. Left of a parameter name: declares parameter pack (binds 0+ args).
  2. 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> and Counter<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+ — constexpr functions:
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::thread uses decay internally 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_of deprecated C++17, removed C++20. Use std::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

  • requires clause 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>'
  • requires expression (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 time MyClass's body is parsed.

Why CRTP

  • Static polymorphism — dispatch resolved at compile time, no vtable.

Use cases:

  1. Inject behavior into a class without runtime cost. Base calls methods derived implements.
  2. Mixins — add capabilities (comparison, hashing, cloning) via inheritance from a CRTP helper.
  3. Counting / registrytemplate <typename T> struct Counted { static int count; ... }; gives each T its own counter.
  4. 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).