πŸ”’ Private Site

This site is password-protected.

Templates & Metaprogramming

Templates are C++’s mechanism for generic programming β€” writing code that works with any type. Metaprogramming takes it further: computing things at compile time.


Table of Contents

1 β€” Function Templates

2 β€” Class Templates

3 β€” Variadic Templates

4 β€” SFINAE

5 β€” Type Traits

6 β€” Compile-Time Computation

7 β€” CRTP

8 β€” Template Argument Deduction

9 β€” Common Template Patterns

Practice Questions


Glossary β€” Key Terms at a Glance

Term Meaning
Template Code parameterized by types and/or values β€” instantiated at compile time
Specialization Override template behavior for specific types (full or partial)
Variadic Template Template that accepts any number of type/value parameters
Fold Expression C++17 syntax to reduce a parameter pack with an operator
SFINAE Substitution Failure Is Not An Error β€” enables/disables overloads
Type Trait Compile-time introspection of type properties
TMP Template Metaprogramming β€” computing with types at compile time
CRTP Curiously Recurring Template Pattern β€” static polymorphism
CTAD Class Template Argument Deduction (C++17)
if constexpr Compile-time branching that discards untaken branches

1 β€” Function Templates

1.1 Basic Function Templates

Definition: A function template is a blueprint for a function parameterized by one or more types. The compiler generates concrete functions for each type used.

template<typename T>
T max_val(T a, T b) {
    return (a > b) ? a : b;
}

max_val(3, 5);            // T = int, returns 5
max_val(3.14, 2.71);      // T = double, returns 3.14
max_val<int>(3.0, 5.0);   // explicit: T = int, converts arguments

πŸ” Why This Logic: The compiler deduces T from the function arguments. Each unique T creates a separate function instance β€” this is called template instantiation.


1.2 Multiple & Non-Type Parameters

Multiple Template Parameters:

template<typename T, typename U>
auto add(T a, U b) {
    return a + b;  // return type deduced (C++14)
}

add(1, 2.5);  // T=int, U=double, returns double

Non-Type Template Parameters:

template<typename T, int N>
class FixedArray {
    T data[N];
public:
    T& operator[](int i) { return data[i]; }
    constexpr int size() const { return N; }
};

FixedArray<double, 100> arr;  // stack-allocated, size known at compile time

Tip: Non-type parameters must be compile-time constants. They enable stack allocation (no heap!) and allow the compiler to optimize based on known sizes.


2 β€” Class Templates

2.1 Class Templates & Specialization

Basic Class Template:

template<typename T>
class Stack {
    std::vector<T> data;
public:
    void push(const T& val) { data.push_back(val); }
    void push(T&& val) { data.push_back(std::move(val)); }
    T& top() { return data.back(); }
    const T& top() const { return data.back(); }
    void pop() { data.pop_back(); }
    bool empty() const { return data.empty(); }
    size_t size() const { return data.size(); }
};

Stack<int> intStack;
Stack<std::string> strStack;

Definition: Template specialization lets you override the default template behavior for specific types.

Full Specialization (one specific type):

// Primary template
template<typename T>
class Printer {
public:
    void print(const T& val) { std::cout << val << "\n"; }
};

// Full specialization for bool
template<>
class Printer<bool> {
public:
    void print(bool val) { std::cout << (val ? "true" : "false") << "\n"; }
};

Partial Specialization (a category of types):

// Partial specialization for pointers
template<typename T>
class Printer<T*> {
public:
    void print(T* val) {
        if (val) std::cout << *val << "\n";
        else std::cout << "nullptr\n";
    }
};

⚠️ Note: Function templates cannot be partially specialized β€” use overloading or if constexpr instead.


3 β€” Variadic Templates

3.1 Recursive Variadic Templates

Definition: Variadic templates accept any number of template arguments using a parameter pack (typename... Args).

// Base case
void print() {
    std::cout << "\n";
}

// Recursive case
template<typename T, typename... Args>
void print(T first, Args... rest) {
    std::cout << first << " ";
    print(rest...);  // recursion with remaining args
}

print(1, "hello", 3.14, true);  // "1 hello 3.14 1"

sizeof... β€” Count Arguments:

template<typename... Args>
void info(Args... args) {
    std::cout << "Got " << sizeof...(Args) << " arguments\n";
}

3.2 Fold Expressions (C++17)

Fold expressions replace recursive variadic patterns with a single line:

// Left fold: ((a + b) + c) + d
template<typename... Args>
auto sum(Args... args) {
    return (... + args);
}

// Right fold: a + (b + (c + d))
template<typename... Args>
auto sum_r(Args... args) {
    return (args + ...);
}

// Fold with initial value
template<typename... Args>
auto sum_init(Args... args) {
    return (0 + ... + args);  // ((0 + a) + b) + c
}

// Print all with separator
template<typename... Args>
void printAll(Args... args) {
    ((std::cout << args << ", "), ...);
    std::cout << "\n";
}

sum(1, 2, 3, 4);  // 10

Why Fold Expressions? They eliminate the need for a base-case overload and recursive expansion. One line replaces 10+ lines of recursive template code.


4 β€” SFINAE

4.1 enable_if & Concepts

Definition: SFINAE (Substitution Failure Is Not An Error) β€” if template argument substitution fails, the compiler silently discards that overload instead of producing an error.

Classic SFINAE with enable_if:

#include <type_traits>

// Enable only for integral types
template<typename T>
typename std::enable_if<std::is_integral<T>::value, T>::type
safe_divide(T a, T b) {
    return b != 0 ? a / b : 0;
}

// C++14: cleaner with enable_if_t
template<typename T>
std::enable_if_t<std::is_floating_point_v<T>, T>
safe_divide(T a, T b) {
    return b != 0.0 ? a / b : 0.0;
}

C++20 Concepts replace SFINAE β€” vastly more readable:

template<std::integral T>
T safe_divide(T a, T b) { return b != 0 ? a / b : 0; }

template<std::floating_point T>
T safe_divide(T a, T b) { return b != 0.0 ? a / b : 0.0; }

πŸ” SFINAE errors produce 100-line error messages. Concepts produce: β€œT does not satisfy integral”. Always prefer Concepts when using C++20.


5 β€” Type Traits

5.1 Built-in & Custom Type Traits

Built-in Type Traits (<type_traits>):

// Type queries
std::is_integral_v<int>;           // true
std::is_floating_point_v<double>;  // true
std::is_pointer_v<int*>;           // true
std::is_const_v<const int>;        // true
std::is_reference_v<int&>;         // true
std::is_same_v<int, int32_t>;      // true (on most platforms)

// Type transformations
std::remove_const_t<const int>;       // int
std::remove_reference_t<int&>;        // int
std::add_pointer_t<int>;              // int*
std::decay_t<const int&>;             // int
std::common_type_t<int, double>;      // double

// Conditional type
std::conditional_t<true, int, double>;   // int
std::conditional_t<false, int, double>;  // double

Custom Type Traits:

// Check if type has a .size() method
template<typename T, typename = void>
struct has_size : std::false_type {};

template<typename T>
struct has_size<T, std::void_t<decltype(std::declval<T>().size())>>
    : std::true_type {};

static_assert(has_size<std::vector<int>>::value);  // true
static_assert(!has_size<int>::value);               // true

πŸ” Why This Logic: std::void_t is the SFINAE probe. If T.size() is valid, the partial specialization matches and inherits true_type. If not, substitution fails and the primary template (inheriting false_type) is used.


6 β€” Compile-Time Computation

6.1 constexpr & TMP

constexpr Functions (preferred):

constexpr int fibonacci(int n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

constexpr int fib10 = fibonacci(10);  // 55, computed at compile time

// constexpr with arrays (C++14)
constexpr auto makeTable() {
    std::array<int, 256> table{};
    for (int i = 0; i < 256; ++i) {
        table[i] = i * i;
    }
    return table;
}

constexpr auto squares = makeTable();  // entire table built at compile time

Template Metaprogramming (TMP) β€” classic approach:

template<int N>
struct Factorial {
    static constexpr int value = N * Factorial<N - 1>::value;
};

template<>
struct Factorial<0> {
    static constexpr int value = 1;
};

static_assert(Factorial<5>::value == 120);

Prefer constexpr functions over TMP for value computation. TMP is still useful for type computation (e.g., building type lists, conditional types).


6.2 if constexpr

Definition: if constexpr evaluates the condition at compile time. Branches that don’t match are not compiled β€” no SFINAE needed.

template<typename T>
std::string to_string_smart(T val) {
    if constexpr (std::is_same_v<T, std::string>) {
        return val;
    } else if constexpr (std::is_arithmetic_v<T>) {
        return std::to_string(val);
    } else {
        return "unknown";
    }
}
// Only the matching branch is compiled β€” no overhead, no errors in dead branches

7 β€” CRTP

7.1 Curiously Recurring Template Pattern

Definition: CRTP β€” a class inherits from a template parameterized by itself. This enables static (compile-time) polymorphism β€” no virtual function overhead.

template<typename Derived>
class Base {
public:
    void interface() {
        static_cast<Derived*>(this)->implementation();
    }
    
    // Default implementation
    void implementation() {
        std::cout << "Default\n";
    }
};

class Concrete : public Base<Concrete> {
public:
    void implementation() {
        std::cout << "Concrete\n";
    }
};

Concrete c;
c.interface();  // "Concrete" β€” no vtable lookup!

Use case: High-performance systems where virtual function overhead matters (trading engines, game loops). CRTP eliminates the vtable indirection β€” the call is resolved at compile time and can be inlined.


8 β€” Template Argument Deduction

8.1 CTAD & Deduction Guides

Class Template Argument Deduction (CTAD, C++17):

std::pair p{1, 2.0};         // std::pair<int, double>
std::vector v{1, 2, 3};      // std::vector<int>
std::optional o{42};          // std::optional<int>
std::tuple t{1, "hi", 3.0};  // std::tuple<int, const char*, double>

Custom Deduction Guides:

template<typename T>
class MyContainer {
    std::vector<T> data;
public:
    MyContainer(std::initializer_list<T> list) : data(list) {}
};

// Deduction guide
template<typename T>
MyContainer(std::initializer_list<T>) -> MyContainer<T>;

MyContainer c{1, 2, 3};  // MyContainer<int>

9 β€” Common Template Patterns

9.1 Tag Dispatch & Policy-Based Design

Tag Dispatch β€” select implementation based on iterator category:

namespace detail {
    template<typename Iter>
    void advance_impl(Iter& it, int n, std::random_access_iterator_tag) {
        it += n;  // O(1)
    }
    
    template<typename Iter>
    void advance_impl(Iter& it, int n, std::input_iterator_tag) {
        for (int i = 0; i < n; ++i) ++it;  // O(n)
    }
}

template<typename Iter>
void advance(Iter& it, int n) {
    detail::advance_impl(it, n,
        typename std::iterator_traits<Iter>::iterator_category{});
}

Policy-Based Design β€” compose behavior via template parameters:

template<typename SortPolicy, typename PrintPolicy>
class DataProcessor {
    SortPolicy sorter;
    PrintPolicy printer;
public:
    void process(std::vector<int>& data) {
        sorter.sort(data);
        printer.print(data);
    }
};

struct QuickSort {
    void sort(std::vector<int>& v) { std::sort(v.begin(), v.end()); }
};
struct ConsolePrint {
    void print(const std::vector<int>& v) {
        for (auto x : v) std::cout << x << " ";
    }
};

DataProcessor<QuickSort, ConsolePrint> dp;

πŸ” Why Policy-Based Design? It gives you the flexibility of runtime polymorphism (swap algorithms) with zero overhead β€” everything is resolved at compile time.


Practice Questions

Q1. Write a function template min_of that works with any type supporting <. What happens if you call it with two different types?

Q2. Explain the difference between full specialization and partial specialization. Give an example of each.

Q3. Write a variadic template count_args that returns the number of arguments at compile time (as a constexpr).

Q4. Rewrite a recursive variadic product(args...) using C++17 fold expressions. Show both versions.

Q5. Explain SFINAE with a concrete example. Then rewrite the same logic with C++20 Concepts.

Q6. Create a custom type trait is_iterable<T> that checks if a type has begin() and end().

Q7. Write a constexpr function to check if a number is prime. Verify it works at compile time using static_assert.

Q8. Explain CRTP. Why does it outperform virtual functions? What is a disadvantage?

Q9. What is CTAD? Write a deduction guide for a custom Wrapper<T> class.

Q10. Compare tag dispatch vs if constexpr vs Concepts for selecting different implementations. When would you use each?


Key Takeaways

  1. Templates = generic programming β€” write once, works for any type
  2. Prefer constexpr over template metaprogramming for value computation
  3. Use if constexpr instead of SFINAE when possible (C++17)
  4. Use Concepts instead of SFINAE (C++20) β€” vastly more readable
  5. CRTP for compile-time polymorphism in performance-critical code
  6. Specialization lets you customize behavior for specific types
  7. Fold expressions (C++17) eliminate recursive variadic template patterns

← Back to C++ Notes