CRTP in C++: Eliminate Virtual Dispatch Costs

The Curiously Recurring Template Pattern (CRTP) is a C++ idiom that achieves compile-time, or static, polymorphism by combining inheritance with template metaprogramming. This article explains the structure of CRTP, examines how dynamic polymorphism incurs runtime overhead through virtual method tables, and demonstrates how CRTP eliminates indirect call costs by resolving function dispatches during compilation.


What is the CRTP Idiom?

CRTP occurs when a class derives from a base class template that takes the derived class itself as a template argument.

The basic structure looks like this:

template <typename Derived>
class Base {
public:
    void interface() {
        // Cast 'this' pointer to the Derived type and call its implementation
        static_cast<Derived*>(this)->implementation();
    }
};

class Derived : public Base<Derived> {
public:
    void implementation() {
        // Concrete logic here
    }
};

Because Base is parameterized with Derived, the base class has compile-time knowledge of the derived class's interface and memory layout. When Base::interface() is executed, it uses static_cast<Derived*>(this) to invoke Derived::implementation() without relying on runtime type identification.


The Cost of Virtual Function Dispatch

Traditional polymorphism in C++ is dynamic and relies on virtual functions. When a class declares a virtual function, the compiler introduces hidden mechanisms:

  1. Vtable Pointer (vptr) Overhead: Every object instance of a polymorphic class carries an extra pointer pointing to a Virtual Method Table (vtable). This increases the memory footprint of objects.
  2. Indirection and Cache Misses: Calling a virtual function requires dereferencing the object's vptr, locating the appropriate function pointer in the vtable, and branching to that address. This extra layer of indirection can cause CPU instruction cache misses and pipeline stalls.
  3. Inlining Prevention: The compiler often cannot determine which override will be called at compile time when accessing objects through pointers or references. Consequently, it cannot inline the function body, preventing further compiler optimizations such as loop unrolling, dead code elimination, and constant propagation.

How CRTP Eliminates Dispatch Costs

CRTP replaces dynamic dispatch with static dispatch, effectively bypassing the vtable mechanism entirely:

1. Compile-Time Resolution

In CRTP, the compiler generates a unique Base<Derived> specialization for each derived type. Because the exact type is hard-coded into the template specialization, the call static_cast<Derived*>(this)->implementation() is bound at compile time to the concrete Derived::implementation() function. No runtime table lookup or vptr dereferencing is performed.

2. Aggressive Inlining

Because the compiler knows the exact target function at compile time, it can inline the implementation directly into the call site. For small, frequently called methods—such as those found in math libraries, matrix operations, or packet parsers—inlining completely eliminates function call overhead and allows the optimizer to optimize the caller and callee together.

3. Zero Memory Overhead

Objects derived using CRTP do not require a vptr. If neither Base nor Derived declares virtual functions, instances of Derived only consume memory for their actual member variables.


Concrete Comparison

Dynamic Polymorphism (Runtime Overhead)

struct Base {
    virtual void step() = 0;
    virtual ~Base() = default;
};

struct Worker : public Base {
    void step() override {
        // Executed via vtable lookup
    }
};

void run(Base& b) {
    b.step(); // Indirect call: cannot be inlined easily
}

CRTP (Zero Runtime Overhead)

template <typename Derived>
struct Base {
    void step() {
        static_cast<Derived*>(this)->step_impl();
    }
};

struct Worker : public Base<Worker> {
    void step_impl() {
        // Concrete logic
    }
};

template <typename T>
void run(Base<T>& b) {
    b.step(); // Direct call: fully inlinable, no vtable lookup
}

Trade-offs of CRTP

While CRTP eliminates virtual dispatch overhead, it is not an absolute replacement for dynamic polymorphism:

  • No Heterogeneous Collections: Because Base<WorkerA> and Base<WorkerB> are distinct, unrelated types, they cannot be stored together in a standard container like std::vector<Base*>.
  • Code Bloat: Each template instantiation creates a new class type, which can increase binary size if many distinct derived classes instantiate the same base template logic.
  • Compile-Time Coupling: Dynamic polymorphism allows implementations to be swapped out without recompiling callers (e.g., via shared libraries). CRTP binds types at compile time, requiring recompilation when implementations change.