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 enable generic programming in C++, allowing you to write type-independent code that the compiler instantiates for specific types at compile time.

1.1 Default Template Arguments

Template parameters can have default values, just like function parameters. If a default is provided, callers can omit that argument.

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>

Default template arguments were added for function templates in C++11:

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

A template template parameter is a template parameter that is itself a template. This allows you to pass a template (not an instantiation) as an 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: Before C++17, the class keyword was required for template template parameters (you could not use typename). Since C++17, both class and typename are allowed:

// 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)

Variadic templates accept a variable number of template arguments. The ... operator serves two roles:

  1. To the left of a parameter name: declares a parameter pack (binds zero or more arguments).
  2. To the right of an expression: unpacks the parameter pack into separate arguments.
#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 introduced fold expressions for a more 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 of a template with different type arguments gets its own copy of static variables. Two instantiations with the same type share the same static variable.

#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;
}

This is because Counter<int> and Counter<double> are completely separate classes generated by the compiler from the same template.


2. Template Metaprogramming

Template metaprogramming (TMP) uses the C++ template system to perform computations at compile time. While modern C++ (constexpr, if constexpr, concepts) has reduced the need for classic TMP, understanding these techniques is still valuable for library code and legacy codebases.

2.1 Compile-Time Computation

Templates can compute values at compile time using recursive specialization. The compiler evaluates the recursion and produces a 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: In C++11 and later, prefer constexpr functions for compile-time computation:

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)

SFINAE is a core principle of template metaprogramming: when a template argument substitution fails, the compiler does not emit an error. Instead, it silently removes that overload from the candidate set and tries other overloads.

This enables conditionally available function 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

std::enable_if is the primary SFINAE tool. It conditionally defines a type member based on a 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 usage 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 provide a much cleaner syntax:

template <std::integral T>
T quadrupleIt(T value) {
    return value * 4;
}

2.4 Type Traits

The <type_traits> header (C++11) provides compile-time type inspection and transformation. Type traits are the building blocks of SFINAE and template metaprogramming.

Common type 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)

Type 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 that happen when passing arguments by value: it removes references, removes cv-qualifiers, converts arrays to pointers, and converts functions to function pointers. It is particularly useful when you need to store a copy of an argument'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)>);

A practical use case: std::thread uses std::decay internally to store copies of the callable and its arguments, ensuring that references and arrays are handled correctly.

2.6 std::result_of / std::invoke_result

These traits deduce the return type of calling a callable with specific argument types.

Important: std::result_of was deprecated in C++17 and removed in C++20. Use std::invoke_result instead.

#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

See cppreference: std::invoke_result for details.


3. Concepts (C++20)

Concepts (C++20) are named compile-time predicates on template parameters. They replace SFINAE-based template constraints with a much cleaner syntax and dramatically improved error messages.

3.1 The requires Clause

A 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; }

If the predicate fails, the function is removed from overload resolution — like SFINAE, but the diagnostic actually tells you what went wrong:

error: no matching function for call to 'add'
note: candidate disabled by failed constraint 'std::integral<T>'

A requires expression (different from a requires clause) tests whether some 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

The standard library provides many ready-to-use concepts 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-specific 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 can also be combined naturally with && and ||:

template <typename T>
requires std::integral<T> && (sizeof(T) >= 4)
T big_int_op(T x);

For code that targets C++20 or later, prefer concepts. SFINAE remains relevant for backward compatibility and for niche cases where concepts can't express the constraint.


4. CRTP (Curiously Recurring Template Pattern)

The CRTP is a common pattern where a class derives from a 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() { /* ... */ }
};

It looks recursive but it isn't — Base<MyClass> is a complete type by the time MyClass's body is parsed.

Why CRTP

CRTP provides static polymorphism — the dispatch is resolved at compile time instead of via a vtable. Use cases:

  1. Inject behavior into a class without runtime cost. The base class can call methods that the derived class implements.
  2. Mixins — add capabilities (comparison, hashing, cloning) to a class through inheritance from a CRTP helper.
  3. Counting / registrytemplate <typename T> struct Counted { static int count; ... }; automatically gives each T its own counter.
  4. Expression templates — building lazy expression trees (numerical libraries like 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

In modern C++, concepts (C++20, § 3) often replace CRTP for static polymorphism — concepts express requirements directly rather than going through inheritance. But CRTP is still useful for mixins (where you want inheritance to inject members and operators).