CRTP vs Virtual Functions: Static Polymorphism Without vtable Overhead

Key takeaways

How the Curiously Recurring Template Pattern gives you static polymorphism, how it compares with virtual functions in flexibility and speed, practical examples, and common CRTP mistakes.

What is CRTP?

The Curiously Recurring Template Pattern looks backwards the first time you see it: a class inherits from a template that is itself parameterized by the derived class. class Derived : public Base<Derived> reads like a typo, but that self-reference is exactly what lets the base class call methods on the derived type without a vtable. The compiler knows the concrete type at compile time, so static_cast<Derived*>(this)->implementation() resolves to a direct, inlineable call instead of an indirect jump through a virtual table.

This matters because virtual dispatch has a real, if small, cost: a pointer chase through the vtable, a lost opportunity for the compiler to inline the call, and (in tight loops) a barrier to vectorization. CRTP trades that runtime flexibility — you can no longer swap implementations behind a Base* pointer at runtime — for compile-time resolution that the optimizer can see through completely. It’s the same idea the Strategy and Factory patterns lean on elsewhere: pick the axis of variation you actually need (compile-time vs. runtime) and stop paying for the one you don’t.

I reached for CRTP the first time not for performance but for the mixin use case below — I had four unrelated classes that all needed reference counting, and I didn’t want a common virtual base pulling in a vtable for classes that were otherwise trivial. That’s usually the more honest motivation in application code; the “2-5x faster than virtual calls” number gets quoted a lot, but it only shows up in hot loops that call the same method millions of times. If your dispatch isn’t in a profiler-visible hot path, CRTP’s real value is the mixin composition, not the speed.

  • Pass derived class as template argument to base class
  • Static polymorphism (compile time)
  • Implement polymorphism without virtual functions
// Base class
template<typename Derived>
class Base {
public:
    void interface() {
        static_cast<Derived*>(this)->implementation();
    }
};
// Derived class
class Derived : public Base<Derived> {
public:
    void implementation() {
        cout << "Derived implementation" << endl;
    }
};
int main() {
    Derived d;
    d.interface();  // "Derived implementation"
}

Virtual Functions vs CRTP

Virtual Functions (Dynamic Polymorphism)

class Base {
public:
    virtual void func() = 0;
    virtual ~Base() {}
};
class Derived : public Base {
public:
    void func() override {
        cout << "Derived" << endl;
    }
};
// Runtime overhead (vtable)

CRTP (Static Polymorphism)

template<typename Derived>
class Base {
public:
    void func() {
        static_cast<Derived*>(this)->funcImpl();
    }
};
class Derived : public Base<Derived> {
public:
    void funcImpl() {
        cout << "Derived" << endl;
    }
};
// Compile time, no overhead

Notice what the base class needs to know in each case. The virtual version needs nothing about Derived — that’s the whole point of runtime polymorphism, and it’s why you’d still reach for it when plugins, DLL boundaries, or a container of heterogeneous Base* pointers are involved. The CRTP version needs Derived to already have funcImpl defined by the time Base<Derived>::func is instantiated, which is fine for a normal call but becomes a real trap if the base class constructor tries to call into the derived class — the derived object isn’t fully constructed yet, so you get undefined behavior instead of a compile error. That one bit me on a mixin where I called a virtual-style “setup” hook from the base constructor; the fix was to move that call to a separate init() method the derived class calls explicitly after construction.


Practical Examples

Example 1: Counter Mixin

template<typename Derived>
class Countable {
private:
    static int count;
    
public:
    Countable() { count++; }
    Countable(const Countable&) { count++; }
    ~Countable() { count--; }
    
    static int getCount() { return count; }
};
template<typename Derived>
int Countable<Derived>::count = 0;
class Widget : public Countable<Widget> {
public:
    Widget() { cout << "Widget created" << endl; }
};
class Gadget : public Countable<Gadget> {
public:
    Gadget() { cout << "Gadget created" << endl; }
};
int main() {
    Widget w1, w2;
    Gadget g1;
    
    cout << "Widget count: " << Widget::getCount() << endl;  // 2
    cout << "Gadget count: " << Gadget::getCount() << endl;  // 1
}

This is the pattern’s clearest win: Countable<Widget> and Countable<Gadget> are two entirely separate types with two entirely separate static int count members, even though they share the exact same source. A single non-template base class with a static counter would give every subclass the same counter, which is rarely what you want. Because the template parameter is baked into the type, each instantiation gets its own storage for free — no runtime registration, no map keyed by type name, just the type system doing the bookkeeping.

Example 2: Auto-Generate Comparison Operators

template<typename Derived>
class Comparable {
public:
    friend bool operator!=(const Derived& lhs, const Derived& rhs) {
        return !(lhs == rhs);
    }
    
    friend bool operator>(const Derived& lhs, const Derived& rhs) {
        return rhs < lhs;
    }
    
    friend bool operator<=(const Derived& lhs, const Derived& rhs) {
        return !(rhs < lhs);
    }
    
    friend bool operator>=(const Derived& lhs, const Derived& rhs) {
        return !(lhs < rhs);
    }
};
class Point : public Comparable<Point> {
public:
    int x, y;
    
    Point(int x, int y) : x(x), y(y) {}
    
    // Only implement == and <, rest auto-generated
    friend bool operator==(const Point& lhs, const Point& rhs) {
        return lhs.x == rhs.x && lhs.y == rhs.y;
    }
    
    friend bool operator<(const Point& lhs, const Point& rhs) {
        if (lhs.x != rhs.x) return lhs.x < rhs.x;
        return lhs.y < rhs.y;
    }
};
int main() {
    Point p1(1, 2);
    Point p2(3, 4);
    
    cout << (p1 == p2) << endl;  // 0
    cout << (p1 != p2) << endl;  // 1
    cout << (p1 < p2) << endl;   // 1
    cout << (p1 >= p2) << endl;  // 0
}

<=> (the spaceship operator, C++20) largely replaces this specific use case for new code — a single auto operator<=>(const Point&) const = default; gives you all six comparisons in one line without CRTP at all. This example is still worth understanding, though, because the underlying trick — inject boilerplate operators as friend functions found via argument-dependent lookup, defined once in the base and reused by every derived type — generalizes to things <=> doesn’t cover, like hashing helpers, serialization boilerplate, or logging wrappers. If you’re on C++20 or later and only need comparisons, use <=>; reach for this CRTP form when you need to inject arbitrary repeated boilerplate, not just comparisons.

Example 3: Singleton Mixin

template<typename Derived>
class Singleton {
protected:
    Singleton() {}
    
public:
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    
    static Derived& getInstance() {
        static Derived instance;
        return instance;
    }
};
class Config : public Singleton<Config> {
    friend class Singleton<Config>;
    
private:
    Config() { cout << "Config created" << endl; }
    int value = 0;
    
public:
    void setValue(int v) { value = v; }
    int getValue() { return value; }
};
class Logger : public Singleton<Logger> {
    friend class Singleton<Logger>;
    
private:
    Logger() { cout << "Logger created" << endl; }
    
public:
    void log(const string& msg) {
        cout << "[LOG] " << msg << endl;
    }
};
int main() {
    Config::getInstance().setValue(100);
    Logger::getInstance().log("System started");
    
    cout << Config::getInstance().getValue() << endl;  // 100
}

The static Derived instance; inside a function is doing the real work here — it’s a function-local static, which the C++11 standard guarantees is initialized exactly once even under concurrent first calls from multiple threads (the so-called “magic statics” guarantee). That’s what makes this version of the singleton pattern safe without any manual mutex or std::call_once. CRTP just factors the boilerplate (deleted copy constructor, deleted assignment, the getInstance() accessor) into a reusable base so you don’t retype it for every singleton in your codebase. The friend class Singleton<Config> line is easy to forget and gives a confusing “constructor is private” error if you do — the base needs friendship to construct the derived type from inside getInstance().

Example 4: Chaining Interface

template<typename Derived>
class Chainable {
protected:
    Derived& self() {
        return static_cast<Derived&>(*this);
    }
};
class QueryBuilder : public Chainable<QueryBuilder> {
private:
    string query;
    
public:
    QueryBuilder& select(const string& fields) {
        query = "SELECT " + fields;
        return self();
    }
    
    QueryBuilder& from(const string& table) {
        query += " FROM " + table;
        return self();
    }
    
    QueryBuilder& where(const string& condition) {
        query += " WHERE " + condition;
        return self();
    }
    
    string build() {
        return query;
    }
};
int main() {
    QueryBuilder qb;
    string sql = qb.select("*")
                   .from("users")
                   .where("age > 18")
                   .build();
    
    cout << sql << endl;
    // SELECT * FROM users WHERE age > 18
}

Fluent builders like this are usually written by having every method return *this; directly, which works fine as long as QueryBuilder never gets subclassed. The moment you want a TypedQueryBuilder<T> : public QueryBuilder that adds type-specific methods while still chaining the base class’s select/from/where, returning *this breaks — it returns a QueryBuilder&, not a TypedQueryBuilder&, so the chain can’t continue with the subclass’s methods. The self() helper here, returning static_cast<Derived&>(*this), is the standard fix: every chained call returns the actual derived type, so subclassing a fluent interface doesn’t quietly downgrade the chain.


Performance Comparison

#include <chrono>
// Virtual functions
class VirtualBase {
public:
    virtual int compute(int x) = 0;
    virtual ~VirtualBase() {}
};
class VirtualDerived : public VirtualBase {
public:
    int compute(int x) override {
        return x * 2;
    }
};
// CRTP
template<typename Derived>
class CRTPBase {
public:
    int compute(int x) {
        return static_cast<Derived*>(this)->computeImpl(x);
    }
};
class CRTPDerived : public CRTPBase<CRTPDerived> {
public:
    int computeImpl(int x) {
        return x * 2;
    }
};
int main() {
    const int N = 100000000;
    
    // Virtual functions
    VirtualBase* vb = new VirtualDerived();
    auto start = chrono::high_resolution_clock::now();
    for (int i = 0; i < N; i++) {
        vb->compute(i);
    }
    auto end = chrono::high_resolution_clock::now();
    cout << "Virtual: " << chrono::duration_cast<chrono::milliseconds>(end - start).count() << "ms" << endl;
    
    // CRTP
    CRTPDerived cd;
    start = chrono::high_resolution_clock::now();
    for (int i = 0; i < N; i++) {
        cd.compute(i);
    }
    end = chrono::high_resolution_clock::now();
    cout << "CRTP: " << chrono::duration_cast<chrono::milliseconds>(end - start).count() << "ms" << endl;
}

What to expect: in a tight loop like this, the CRTP version usually comes out ahead, because the compiler knows the concrete type, can inline compute(), and can then optimize the whole loop body; the virtual version needs an indirect call per iteration that blocks inlining.

Take that gap with a grain of salt, though — it’s measuring the best possible case for CRTP: a microbenchmark where the same call happens 100 million times in a loop the optimizer can see end to end. In real code, the compiler often can’t inline through a virtual call anyway (the call site doesn’t know the concrete type), so the relative gap looks big in isolation. But if compute() itself does meaningful work — a database query, a network call, anything beyond a few arithmetic ops — the dispatch overhead disappears into the noise and the choice between CRTP and virtual functions should be made on design grounds (do you need runtime polymorphism or not), not on this benchmark.


Common Issues

Issue 1: Wrong casting

// ❌ Dangerous
template<typename Derived>
class Base {
public:
    void func() {
        static_cast<Derived*>(this)->impl();
    }
};
class Wrong : public Base<OtherClass> {  // Wrong type!
public:
    void impl() {}
};
// ✅ Correct usage
class Correct : public Base<Correct> {
public:
    void impl() {}
};

The dangerous part of this mistake is that it often compiles cleanly. static_cast<OtherClass*>(this) is a valid cast as far as the compiler is concerned if OtherClass derives from Base<OtherClass> somewhere in your codebase — it just casts this (which is actually a Wrong*) to a completely unrelated type, and calling impl() on that pointer is undefined behavior that might work by accident in a debug build and corrupt memory in release. There’s no built-in way to static_assert this in the general case, though some codebases add a static_assert(std::is_base_of_v<Base<Derived>, Derived>) inside the base to catch the most common variant of the mistake at compile time.

Issue 2: Circular dependency

// ❌ Circular dependency
class A : public Base<B> {};  // B not yet defined
class B : public Base<A> {};
// ✅ Each uses itself as template argument
class A : public Base<A> {};
class B : public Base<B> {};

This one is a genuine circular dependency in the sense of “which type needs to be complete first,” not just a naming confusion — Base<B> needs B’s definition to instantiate methods that call into it, but B needs Base<B>’s definition to inherit from it. The fix (each class parameterizes itself, not another not-yet-defined class) works because CRTP bases only need the derived type to be declared, not fully defined, at the point of inheritance — the methods that actually call into Derived aren’t instantiated until they’re used, by which point the compiler has seen the full class body.

Issue 3: Missing virtual destructor

// ❌ Possible memory leak
template<typename Derived>
class Base {
    // No virtual destructor
};
// ✅ Add virtual destructor (for polymorphic deletion)
template<typename Derived>
class Base {
public:
    virtual ~Base() = default;
};

This is worth flagging because it’s counterintuitive: if you’re going through the trouble of avoiding virtual functions for performance, adding a virtual destructor back in seems to defeat the point. It doesn’t, in practice — a virtual destructor costs one vtable pointer per object and one indirect call at destruction time only, which almost never shows up in a profile. The real risk it guards against is deleting a derived object through a Base* (for example, storing CRTP objects in a std::vector<std::unique_ptr<Base<T>>>-style container erased to a common base) — without a virtual destructor, only ~Base() runs and any derived-class members leak. If you never delete through a base pointer — which is common in pure CRTP code where the base is never used polymorphically at runtime — you can safely skip it and keep the destructor non-virtual.


C++23 alternative: deducing this

Much of what CRTP is used for, a base class that calls into the derived class without virtual functions, can be written more directly in C++23 with an explicit object parameter (“deducing this”):

struct Printable {
    template <class Self>
    void print(this const Self& self) {   // Self is the most-derived type
        self.printImpl();
    }
};
struct Point : Printable {
    int x = 1, y = 2;
    void printImpl() const { std::cout << x << ", " << y << '\n'; }
};
Point{}.print();   // calls Point::printImpl, no virtual call, no template base

The base is no longer a template, so there is no Derived parameter to repeat, no static_cast<Derived&>(*this), and no risk of the classic CRTP mistake of passing the wrong class as the template argument (struct B : Base<A>). The limitation is compiler support: explicit object parameters need GCC 14, Clang 18, or MSVC 17.2 or newer. CRTP remains the portable choice for code that must build with older toolchains, and it is still what most existing libraries use, so it is worth recognizing even if you write new code with deducing this.

CRTP Use Scenarios

Performance-critical cases

// Game engines, high-performance computing
template<typename Derived>
class Entity {
public:
    void update(float dt) {
        static_cast<Derived*>(this)->updateImpl(dt);
    }
};

Code reuse

// Common functionality as mixin
template<typename Derived>
class Serializable {
public:
    string serialize() {
        // Serialization logic
    }
};

Compile-time polymorphism

// Polymorphism via template argument
template<typename T>
void process(T& obj) {
    obj.compute();  // Decided at compile time
}

FAQ

Q1: When to use CRTP?

A:

  • Performance-critical cases
  • Compile-time polymorphism needed
  • Mixin patterns

Q2: Always CRTP instead of virtual functions?

A: No. Use virtual functions when runtime polymorphism needed.

Q3: CRTP disadvantages?

A:

  • Increased code complexity
  • Longer compile times
  • No runtime polymorphism

Q4: What are mixins?

A: Pattern combining multiple base classes to add functionality.

Q5: CRTP vs Template Method pattern?

A: CRTP is static, Template Method is dynamic.

Q6: CRTP learning resources?

A:

  • “Modern C++ Design” (Andrei Alexandrescu)
  • “C++ Templates: The Complete Guide”
  • Boost library source code