C++ CRTP Complete Guide | Static Polymorphism and Compile-Time Optimization

C++ CRTP Complete Guide | Static Polymorphism and Compile-Time Optimization

이 글의 핵심

C++ CRTP: static polymorphism and compile-time optimization. What is CRTP?. Why needed·basic structure.

What is CRTP? Why Needed?

Problem Scenario: Runtime Overhead of Virtual Functions

Problem: Virtual functions provide runtime polymorphism, but performance degrades due to vtable lookup cost and inability to inline. Here is detailed implementation code using C++. Define a class to encapsulate data and functionality. Understand the role of each part while examining the code.

// Virtual function (runtime polymorphism)
// Type definition
class Shape {
public:
    virtual double area() const = 0;  // vtable lookup
};
class Circle : public Shape {
public:
    double area() const override {
        return 3.14159 * radius * radius;
    }
private:
    double radius;
};
void process(const Shape& s) {
    double a = s.area();  // vtable lookup at runtime
}

Solution: CRTP (Curiously Recurring Template Pattern) provides compile-time polymorphism. By receiving derived class as template argument and calling via static_cast, inline optimization is possible without vtable. Here is detailed implementation code using C++. Define a class to encapsulate data and functionality. Understand the role of each part while examining the code.

// CRTP (compile-time polymorphism)
// Execution example
template<typename Derived>
class Shape {
public:
    double area() const {
        return static_cast<const Derived*>(this)->areaImpl();
    }
};
class Circle : public Shape<Circle> {
public:
    double areaImpl() const {
        return 3.14159 * radius * radius;
    }
private:
    double radius;
};
void process(const auto& s) {
    double a = s.area();  // Inlined at compile-time
}

1. Basic Structure

Minimal CRTP

Here is detailed implementation code using C++. Import the necessary modules and define a class to encapsulate data and functionality. Understand the role of each part while examining the code.

#include <iostream>
template<typename Derived>
class Base {
public:
    void interface() {
        static_cast<Derived*>(this)->implementation();
    }
};
class Derived : public Base<Derived> {
public:
    void implementation() {
        std::cout << "Derived implementation\n";
    }
};
int main() {
    Derived d;
    d.interface();  // "Derived implementation"
}

Key: Base receives Derived as template argument and calls derived class method via static_cast<Derived*>(this).

2. Interface Enforcement

Shape Example

Here is detailed implementation code using C++. Import the necessary modules and define a class to encapsulate data and functionality. Understand the role of each part while examining the code.

#include <iostream>
#include <cmath>
template<typename Derived>
class Shape {
public:
    double area() const {
        return static_cast<const Derived*>(this)->areaImpl();
    }
    
    void draw() const {
        static_cast<const Derived*>(this)->drawImpl();
    }
};
class Circle : public Shape<Circle> {
public:
    Circle(double r) : radius(r) {}
    
    double areaImpl() const {
        return M_PI * radius * radius;
    }
    
    void drawImpl() const {
        std::cout << "Drawing circle with radius " << radius << '\n';
    }
    
private:
    double radius;
};
class Rectangle : public Shape<Rectangle> {
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    
    double areaImpl() const {
        return width * height;
    }
    
    void drawImpl() const {
        std::cout << "Drawing rectangle " << width << "x" << height << '\n';
    }
    
private:
    double width, height;
};
template<typename T>
void processShape(const Shape<T>& s) {
    std::cout << "Area: " << s.area() << '\n';
    s.draw();
}
int main() {
    Circle c(5.0);
    Rectangle r(4.0, 6.0);
    
    processShape(c);  // Area: 78.5398, Drawing circle...
    processShape(r);  // Area: 24, Drawing rectangle...
}

Advantage: If Circle and Rectangle don’t implement areaImpl(), drawImpl(), compile error occurs, enforcing interface.

3. Static Counter

Tracking Object Count

Here is detailed implementation code using C++. Import the necessary modules and define a class to encapsulate data and functionality. Understand the role of each part while examining the code.

#include <iostream>
template<typename Derived>
class Counter {
public:
    Counter() { ++count; }
    Counter(const Counter&) { ++count; }
    ~Counter() { --count; }
    
    static int getCount() { return count; }
    
private:
    static inline int count = 0;
};
class Widget : public Counter<Widget> {};
class Gadget : public Counter<Gadget> {};
int main() {
    {
        Widget w1, w2;
        Gadget g1;
        std::cout << "Widgets: " << Widget::getCount() << '\n';  // 2
        std::cout << "Gadgets: " << Gadget::getCount() << '\n';  // 1
    }
    std::cout << "Widgets: " << Widget::getCount() << '\n';  // 0
    std::cout << "Gadgets: " << Gadget::getCount() << '\n';  // 0
}

Key: Each derived class has independent static counter because template instantiation creates separate Counter<Widget> and Counter<Gadget>.

Summary

Key Points

  1. CRTP: Curiously Recurring Template Pattern
  2. Static polymorphism: Compile-time polymorphism without vtable
  3. Performance: No runtime overhead, inline optimization possible
  4. Interface enforcement: Compile-time interface checking
  5. Static members: Each derived class has independent static members

When to Use

Use CRTP when:

  • Need compile-time polymorphism
  • Performance is critical (no vtable overhead)
  • Interface enforcement at compile-time
  • Static member per derived class ❌ Don’t use when:
  • Need runtime polymorphism
  • Heterogeneous containers required
  • Code complexity outweighs benefits

Best Practices

  • ✅ Use for compile-time optimization
  • ✅ Enforce interface at compile-time
  • ✅ Implement static counters/IDs
  • ❌ Don’t overuse (increases code complexity)
  • ❌ Don’t use for runtime polymorphism