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
- CRTP: Curiously Recurring Template Pattern
- Static polymorphism: Compile-time polymorphism without vtable
- Performance: No runtime overhead, inline optimization possible
- Interface enforcement: Compile-time interface checking
- 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
Related Articles
- C++ CRTP Pattern
- C++ Mixin
- C++ Compile-Time Optimization Master CRTP for zero-overhead abstraction! 🚀