Skip to content

Template Metaprogramming Patterns

Master advanced template patterns: CRTP for static polymorphism, policy-based design for flexible composition, type erasure for runtime generics, and guidelines for when to use templates vs virtual dispatch.

Beyond Basic Templates

Templates enable design patterns that have no equivalent in other languages. These patterns exploit the compile-time nature of templates to achieve static polymorphism (no virtual function overhead), composable behavior (policies), and type-safe erasure (hiding concrete types behind a uniform interface).

These are not beginner patterns — they are tools for library authors and framework designers. Understanding them is essential for reading standard library internals, Boost, and modern C++ frameworks.

CRTP: Curiously Recurring Template Pattern

In CRTP, a class derives from a template instantiated with itself as the argument: class Derived : public Base. The base class can call methods on the derived class at compile time — no virtual dispatch overhead. This is used to inject behavior, implement static polymorphism, and create mixin functionality.

crtp.cpp
#include <iostream>

// CRTP base: provides a counter for each derived class
template <typename Derived>
class Counter {
    static inline int count_ = 0;

protected:
    Counter()  { ++count_; }
    ~Counter() { --count_; }
    Counter(const Counter&) { ++count_; }

public:
    static int alive() { return count_; }
};

// Each derived class gets its OWN counter
class Dog : public Counter<Dog> {
public:
    std::string name;
    Dog(std::string n) : name(std::move(n)) {}
};

class Cat : public Counter<Cat> {
public:
    std::string name;
    Cat(std::string n) : name(std::move(n)) {}
};

// CRTP for static polymorphism: no virtual dispatch
template <typename Derived>
class Shape {
public:
    double area() const {
        // Call derived class method — resolved at compile time
        return static_cast<const Derived*>(this)->area_impl();
    }
};

class Circle : public Shape<Circle> {
    double radius_;
public:
    Circle(double r) : radius_(r) {}
    double area_impl() const { return 3.14159265 * radius_ * radius_; }
};

class Square : public Shape<Square> {
    double side_;
public:
    Square(double s) : side_(s) {}
    double area_impl() const { return side_ * side_; }
};

int main() {
    Dog d1("Rex"), d2("Buddy");
    Cat c1("Whiskers");
    std::cout << "Dogs alive: " << Dog::alive() << '\n'; // 2
    std::cout << "Cats alive: " << Cat::alive() << '\n'; // 1

    Circle circle(5.0);
    Square square(4.0);
    std::cout << "Circle area: " << circle.area() << '\n'; // 78.54
    std::cout << "Square area: " << square.area() << '\n'; // 16.0
}

Policy-Based Design

Policy-based design decomposes a class into interchangeable behavior components (policies) passed as template parameters. Each policy implements one aspect of behavior. Users compose the exact combination they need. This was popularized by Andrei Alexandrescu's "Modern C++ Design."

policy_design.cpp
#include <iostream>
#include <mutex>
#include <stdexcept>

// Threading policies
struct SingleThreaded {
    struct Lock { Lock(SingleThreaded&) {} }; // no-op
};

struct MultiThreaded {
    std::mutex mtx;
    struct Lock {
        std::lock_guard<std::mutex> guard;
        Lock(MultiThreaded& host) : guard(host.mtx) {}
    };
};

// Bounds checking policies
struct NoBoundsCheck {
    static void check(int /*index*/, int /*size*/) {} // no-op
};

struct StrictBoundsCheck {
    static void check(int index, int size) {
        if (index < 0 || index >= size)
            throw std::out_of_range("Index out of bounds");
    }
};

// Policy-based container: users choose their combination
template <typename T,
         typename ThreadingPolicy = SingleThreaded,
         typename BoundsPolicy = NoBoundsCheck>
class SmartArray : private ThreadingPolicy {
    T* data_;
    int size_;

public:
    SmartArray(int size) : data_(new T[size]{}), size_(size) {}
    ~SmartArray() { delete[] data_; }

    T& operator[](int index) {
        typename ThreadingPolicy::Lock lock(*this);
        BoundsPolicy::check(index, size_);
        return data_[index];
    }

    int size() const { return size_; }
};

int main() {
    // Fast, no overhead: no locking, no bounds checks
    SmartArray<int> fast(10);
    fast[0] = 42;

    // Safe: bounds-checked, single-threaded
    SmartArray<int, SingleThreaded, StrictBoundsCheck> safe(10);
    safe[0] = 42;
    try {
        safe[99] = 0; // throws!
    } catch (const std::out_of_range& e) {
        std::cout << e.what() << '\n';
    }

    // Thread-safe with bounds checking
    SmartArray<int, MultiThreaded, StrictBoundsCheck> robust(10);
    robust[5] = 100;
}

Type Erasure

Type erasure hides the concrete type behind a uniform interface, combining the flexibility of templates with the runtime polymorphism of virtual functions. The classic example is std::function, which can hold any callable. The pattern uses three components: a concept (abstract interface), a model (template wrapper that implements the interface for any type), and an outer handle class.

type_erasure.cpp
#include <iostream>
#include <memory>
#include <string>

// Type-erased "Printable" — can hold any type with a print() method
class Printable {
    // Concept: abstract interface
    struct Concept {
        virtual ~Concept() = default;
        virtual void print(std::ostream& os) const = 0;
        virtual std::unique_ptr<Concept> clone() const = 0;
    };

    // Model: wraps any T that supports operator<<
    template <typename T>
    struct Model : Concept {
        T value;
        Model(T v) : value(std::move(v)) {}
        void print(std::ostream& os) const override { os << value; }
        std::unique_ptr<Concept> clone() const override {
            return std::make_unique<Model>(value);
        }
    };

    std::unique_ptr<Concept> ptr_;

public:
    // Constructor: accepts any printable type
    template <typename T>
    Printable(T value) : ptr_(std::make_unique<Model<T>>(std::move(value))) {}

    // Copy support
    Printable(const Printable& other) : ptr_(other.ptr_->clone()) {}
    Printable& operator=(const Printable& other) {
        ptr_ = other.ptr_->clone();
        return *this;
    }
    Printable(Printable&&) = default;
    Printable& operator=(Printable&&) = default;

    friend std::ostream& operator<<(std::ostream& os, const Printable& p) {
        p.ptr_->print(os);
        return os;
    }
};

int main() {
    // Heterogeneous collection — no common base class needed!
    std::vector<Printable> items;
    items.emplace_back(42);
    items.emplace_back(3.14);
    items.emplace_back(std::string("hello"));
    items.emplace_back('X');

    for (const auto& item : items) {
        std::cout << item << '\n';
    }
    // Output: 42, 3.14, hello, X
}

Expression Templates (Overview)

Expression templates are a technique where arithmetic expressions on objects build a compile-time tree of operations rather than computing intermediate results. The final result is computed in a single pass, eliminating temporary objects.

For example, with a naive Vector class, a + b + c creates two temporary vectors. With expression templates, a + b + c builds a lightweight expression object Add, Vector> that computes elements on demand — no temporaries needed.

This technique is used by high-performance linear algebra libraries like Eigen and Blaze. The expression tree is evaluated lazily, and the compiler can often vectorize the resulting single-pass loop.

Expression templates are complex to implement correctly and are primarily a library-author concern. If you need matrix/vector math, use a library like Eigen rather than implementing expression templates yourself.

Best Practice

When choosing between templates (static polymorphism) and virtual functions (dynamic polymorphism):

Use templates when:
- The set of types is known at compile time
- You need zero-overhead abstractions (no vtable, no indirection)
- You want the compiler to inline and optimize across type boundaries
- You need to work with value semantics (no heap allocation)
- Performance is critical (tight loops, numerical code)

Use virtual functions when:
- The set of types is open and determined at runtime (plugins, user input)
- You need heterogeneous containers (e.g., vector>)
- Binary size matters more than speed (templates generate more code)
- You want stable ABI across shared library boundaries
- You need to add new types without recompiling existing code

Use type erasure when:
- You want the interface simplicity of virtual dispatch but don't want to force users to inherit from your base class
- You need value semantics with runtime polymorphism (std::function, std::any)

Pitfall

Template metaprogramming is powerful but has real costs:

1. Compile-time explosion — deeply nested template instantiations can make compile times unacceptable. Recursive TMP is especially bad — prefer fold expressions and if constexpr.
2. Error messages — without concepts, template errors produce pages of incomprehensible text. Always constrain templates.
3. Debugging — stepping through template-heavy code in a debugger is painful. Type-erased wrappers add indirection that hides the actual type.
4. Readability — CRTP, policy-based design, and expression templates are hard for junior developers to understand. Document the pattern name and intent.
5. Overengineering — not every problem needs a template solution. A simple virtual function or even a switch statement may be clearer and sufficient.

Key Takeaways
  • CRTP enables static polymorphism and per-class counters/mixins by deriving from Base
  • Policy-based design decomposes behavior into template parameters — users compose the exact combination they need
  • Type erasure combines template flexibility with runtime polymorphism (e.g., std::function, std::any)
  • Expression templates eliminate temporaries in chained operations — used by Eigen, Blaze, and similar libraries
  • Choose templates for known types and zero overhead; virtual dispatch for open type sets and runtime flexibility
  • Avoid overengineering — use the simplest mechanism that meets your requirements

Quiz — Test Your Knowledge

(20 XP)

1. In CRTP, what does `class Dog : public Counter<Dog>` achieve?

2. What problem does type erasure solve?

3. When should you prefer virtual functions over templates?