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:
- 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. - Indirection and Cache Misses: Calling a virtual
function requires dereferencing the object's
vptr, locating the appropriate function pointer in thevtable, and branching to that address. This extra layer of indirection can cause CPU instruction cache misses and pipeline stalls. - 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>andBase<WorkerB>are distinct, unrelated types, they cannot be stored together in a standard container likestd::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.