Skip to Content
Course content

119: CRTP and Compile-Time Polymorphism

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I remember the first time I tried to implement the Curiously Recurring Template Pattern (CRTP). I was trying to squeeze every last drop of performance out of a physics engine, and I decided that virtual functions—and their associated vtable lookups—were just too expensive for my inner loop. I wanted the "interface" of polymorphism but the speed of a direct function call.

I wrote what I thought was a clever base class, but the compiler absolutely hated it. Here is the kind of code I wrote back then (and the kind of code you'll likely write the first time you try this):

template <typename Derived>
class Shape {
public:
    void draw() {
        // I want the derived class to handle the actual drawing logic
        this->draw_impl(); 
    }
};

class Circle : public Shape<Circle> {
public:
    void draw_impl() {
        // Circle-specific drawing logic
    }
};

int main() {
    Circle c;
    c.draw(); // Compiler Error: 'draw_impl' is not a member of 'Shape<Circle>'
}

The Missing Link Between Base and Derived

If you look at that code, it seems logically sound. Circle inherits from Shape<Circle>, so Circle should have access to draw(). But when the compiler looks at Shape::draw(), it has no idea that this is actually a Circle. In the eyes of the compiler, this is just a pointer to Shape<Circle>, and Shape doesn't have a draw_impl() method.

Unlike virtual functions, where the runtime looks up the correct function in a vtable, CRTP happens at compile time. We have to explicitly tell the compiler: "I know this Shape is actually a Derived type; please treat it as such."

Using static_cast to Enable Static Polymorphism

The fix is a static_cast. Since we've designed the class so that Shape is parameterized by the Derived class, we can safely cast this to the derived type. This allows the compiler to resolve the function call at compile time, which often allows the function to be inlined entirely.

template <typename Derived>
class Shape {
public:
    void draw() {
        // We cast 'this' to the template parameter 'Derived'
        static_cast<Derived*>(this)->draw_impl(); 
    }
};

class Circle : public Shape<Circle> {
public:
    void draw_impl() {
        // Actual implementation
    }
};

class Square : public Shape<Square> {
public:
    void draw_impl() {
        // Actual implementation
    }
};

Now, when you call c.draw(), the compiler sees that Derived is Circle. It replaces the static_cast with a direct call to Circle::draw_impl(). There is no vtable, no pointer chasing, and zero runtime overhead.

Trading Runtime Flexibility for Raw Speed

You might be wondering, "Why not just use virtual?" Well, if you are calling a function millions of times per second in a tight loop, that tiny overhead of a vtable lookup adds up. More importantly, virtual functions usually cannot be inlined because the compiler doesn't know which override will be called until the program is actually running.

However, there is a catch. CRTP is static polymorphism. This means you cannot do this:

// This WILL NOT work with CRTP
std::vector<Shape*> shapes; 
shapes.push_back(new Circle());
shapes.push_back(new Square());

Why? Because Shape<Circle> and Shape<Square> are two completely different, unrelated types. They don't share a common non-templated base class. If you need a heterogeneous container (a list of different shapes), you're stuck with traditional virtual functions. But if you know the type at compile time, or you're using CRTP to implement a mixin or a policy-based design, the performance gains are massive.




📋 Practical Task

Implementing a High-Performance Packet Processor

You are building a network stack where different types of packets (TCP, UDP, ICMP) need to be processed using a common interface, but the processing happens in a high-frequency loop where performance is critical.

Your Task:

  • Create a CRTP base class called PacketProcessor.
  • The base class should have a public method process() that calls a private/protected method do_process() on the derived class using the CRTP pattern.
  • Implement two derived classes: TCPProcessor and UDPProcessor.
  • Each derived class should implement do_process() to print a unique message (e.g., "Processing TCP packet..." and "Processing UDP packet...").
  • In your main() function, instantiate both processors and call process() on each to verify that the static polymorphism is working correctly.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.