The Complete Guide to C++ CRTP | Static Polymorphism and Compile-Time Optimization
이 글의 핵심
CRTP: static polymorphism via base<Derived>, curiously recurring template—zero-cost alternatives to virtuals when types are known at compile time.
What is CRTP? Why Do We Need It?
Problem Scenario: Runtime Overhead of Virtual Functions
The Problem: Virtual functions provide runtime polymorphism, but they degrade performance due to vtable lookup costs and lack of inlining.
// Virtual function (runtime polymorphism)
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
}
The Solution: CRTP (Curiously Recurring Template Pattern) provides compile-time polymorphism. By passing the derived class as a template parameter and using static_cast for calls, it eliminates the need for a vtable and enables inline optimization.
// CRTP (compile-time polymorphism)
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
}
flowchart TD
subgraph virtual["Virtual Function (Runtime)"]
v1["Shape* ptr"]
v2["ptr->area()"]
v3["vtable lookup"]
v4["Circle::area() called"]
end
subgraph crtp["CRTP (Compile-Time)"]
c1["Shape<Circle> obj"]
c2["obj.area()"]
c3["static_cast<Circle*>(this)"]
c4["Circle::areaImpl() inlined"]
end
v1 --> v2 --> v3 --> v4
c1 --> c2 --> c3 --> c4
Table of Contents
- Basic Structure
- Enforcing Interfaces
- Static Counter
- Mixin and CRTP
- Common Errors and Solutions
- Production Patterns
- Complete Example: Mathematical Operators
- Performance Comparison
1. Basic Structure
Minimal CRTP
#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 Point: Base takes Derived as a template parameter and calls the derived class method using static_cast<Derived*>(this).
How static_cast Works in CRTP
The mechanism: When you write Base<Derived>, the compiler knows at compile-time that this (which is Base*) actually points to a Derived object. The static_cast<Derived*>(this) is a compile-time cast with zero runtime overhead.
template<typename Derived>
class Base {
public:
void interface() {
// At compile-time, compiler knows:
// - this is Base<Derived>*
// - Derived inherits from Base<Derived>
// - Therefore, this actually points to a Derived object
// - static_cast is safe and has zero cost
static_cast<Derived*>(this)->implementation();
}
};
Assembly comparison:
// Virtual function
class VirtualBase {
public:
virtual void func() = 0;
};
class VirtualDerived : public VirtualBase {
public:
void func() override { /* ... */ }
};
void call_virtual(VirtualBase* b) {
b->func(); // mov rax, [rdi] ; Load vtable pointer
// call [rax + offset] ; Indirect call through vtable
}
// CRTP
template<typename Derived>
class CRTPBase {
public:
void func() {
static_cast<Derived*>(this)->funcImpl();
}
};
class CRTPDerived : public CRTPBase<CRTPDerived> {
public:
void funcImpl() { /* ... */ }
};
void call_crtp(CRTPDerived* d) {
d->func(); // call CRTPDerived::funcImpl ; Direct call, inlined
}
Performance impact:
- Virtual function: 2-3 CPU cycles for vtable lookup + indirect jump (prevents inlining)
- CRTP: 0 cycles (direct call, can be inlined)
When to use each:
| Feature | Virtual Functions | CRTP |
|---|---|---|
| Polymorphism | Runtime | Compile-time |
| Performance | Slower (vtable) | Faster (inlined) |
| Flexibility | Heterogeneous containers | Homogeneous only |
| Use case | Different types in same container | Performance-critical code |
2. Enforcing Interfaces
Shape Example
#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 or Rectangle fails to implement areaImpl() or drawImpl(), a compile-time error will occur, enforcing the interface.
3. Static Counter
Tracking Object Count
#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 Point: Each derived class has its own independent count variable due to template instantiation.
4. Mixin and CRTP
Combining Features
#include <iostream>
#include <string>
template<typename Derived>
class Printable {
public:
void print() const {
std::cout << static_cast<const Derived*>(this)->toString() << '\n';
}
};
template<typename Derived>
class Comparable {
public:
bool operator<(const Derived& other) const {
return static_cast<const Derived*>(this)->compare(other) < 0;
}
bool operator>(const Derived& other) const {
return other < *static_cast<const Derived*>(this);
}
bool operator==(const Derived& other) const {
return !(*static_cast<const Derived*>(this) < other) &&
!(other < *static_cast<const Derived*>(this));
}
};
class Person : public Printable<Person>, public Comparable<Person> {
public:
Person(std::string n, int a) : name(n), age(a) {}
std::string toString() const {
return name + " (" + std::to_string(age) + ")";
}
int compare(const Person& other) const {
return age - other.age;
}
private:
std::string name;
int age;
};
int main() {
Person p1("Alice", 30);
Person p2("Bob", 25);
p1.print(); // "Alice (30)"
if (p1 > p2) {
std::cout << "Alice is older\n";
}
}
Advantage: Printable and Comparable can be independently combined.
5. Common Errors and Solutions
Error 1: Forgetting to Implement Required Methods
template<typename Derived>
class Base {
public:
void interface() {
static_cast<Derived*>(this)->implementation();
}
};
class Derived : public Base<Derived> {
// ❌ Forgot to implement implementation()
};
int main() {
Derived d;
d.interface(); // Compile error: no member named 'implementation'
}
Solution: Use static_assert to enforce interface at compile-time:
template<typename Derived>
class Base {
public:
void interface() {
static_assert(requires(Derived d) { d.implementation(); },
"Derived must implement implementation()");
static_cast<Derived*>(this)->implementation();
}
};
Error 2: Wrong Template Parameter
// ❌ Wrong: passing Base instead of Derived
class Derived : public Base<Base> { // Wrong!
void implementation() { /* ... */ }
};
// ✅ Correct: passing Derived
class Derived : public Base<Derived> {
void implementation() { /* ... */ }
};
Error 3: Slicing
template<typename Derived>
class Base {
public:
void interface() {
static_cast<Derived*>(this)->implementation();
}
};
class Derived : public Base<Derived> {
public:
void implementation() { std::cout << "Derived\n"; }
};
void process(Base<Derived> b) { // ❌ Pass by value = slicing!
b.interface(); // UB! 'this' is not actually a Derived*
}
void process_correct(Base<Derived>& b) { // ✅ Pass by reference
b.interface(); // OK
}
6. Production Patterns
Pattern 1: Operator Overloading
template<typename Derived>
class Comparable {
public:
friend bool operator<(const Derived& lhs, const Derived& rhs) {
return lhs.compare(rhs) < 0;
}
friend bool operator>(const Derived& lhs, const Derived& rhs) {
return rhs < lhs;
}
friend bool operator<=(const Derived& lhs, const Derived& rhs) {
return !(lhs > rhs);
}
friend bool operator>=(const Derived& lhs, const Derived& rhs) {
return !(lhs < rhs);
}
friend bool operator==(const Derived& lhs, const Derived& rhs) {
return lhs.compare(rhs) == 0;
}
friend bool operator!=(const Derived& lhs, const Derived& rhs) {
return !(lhs == rhs);
}
};
class Number : public Comparable<Number> {
public:
explicit Number(int v) : value(v) {}
int compare(const Number& other) const {
return value - other.value;
}
private:
int value;
};
int main() {
Number a(10), b(20);
std::cout << (a < b) << '\n'; // true
std::cout << (a == b) << '\n'; // false
std::cout << (a >= b) << '\n'; // false
}
Pattern 2: Method Chaining
template<typename Derived>
class Chainable {
public:
Derived& self() {
return static_cast<Derived&>(*this);
}
const Derived& self() const {
return static_cast<const Derived&>(*this);
}
};
class Builder : public Chainable<Builder> {
public:
Builder& setName(std::string n) {
name = n;
return self();
}
Builder& setAge(int a) {
age = a;
return self();
}
void build() {
std::cout << name << ", " << age << '\n';
}
private:
std::string name;
int age;
};
int main() {
Builder()
.setName("Alice")
.setAge(30)
.build(); // "Alice, 30"
}
Pattern 3: Static Interface Enforcement
template<typename Derived>
class Interface {
public:
void execute() {
// Compile-time check
static_assert(requires(Derived d) {
{ d.step1() } -> std::same_as<void>;
{ d.step2() } -> std::same_as<int>;
}, "Derived must implement step1() and step2()");
auto& derived = static_cast<Derived&>(*this);
derived.step1();
int result = derived.step2();
std::cout << "Result: " << result << '\n';
}
};
class Implementation : public Interface<Implementation> {
public:
void step1() { std::cout << "Step 1\n"; }
int step2() { return 42; }
};
7. Complete Example: Mathematical Operators
#include <iostream>
#include <cmath>
template<typename Derived>
class Numeric {
public:
Derived& operator+=(const Derived& other) {
auto& self = static_cast<Derived&>(*this);
self.add(other);
return self;
}
Derived& operator-=(const Derived& other) {
auto& self = static_cast<Derived&>(*this);
self.subtract(other);
return self;
}
friend Derived operator+(Derived lhs, const Derived& rhs) {
lhs += rhs;
return lhs;
}
friend Derived operator-(Derived lhs, const Derived& rhs) {
lhs -= rhs;
return lhs;
}
};
class Vector2D : public Numeric<Vector2D> {
public:
Vector2D(double x, double y) : x_(x), y_(y) {}
void add(const Vector2D& other) {
x_ += other.x_;
y_ += other.y_;
}
void subtract(const Vector2D& other) {
x_ -= other.x_;
y_ -= other.y_;
}
double magnitude() const {
return std::sqrt(x_ * x_ + y_ * y_);
}
void print() const {
std::cout << "(" << x_ << ", " << y_ << ")\n";
}
private:
double x_, y_;
};
int main() {
Vector2D v1(3, 4);
Vector2D v2(1, 2);
auto v3 = v1 + v2;
v3.print(); // (4, 6)
std::cout << "Magnitude: " << v3.magnitude() << '\n'; // 7.211...
}
8. Performance Comparison
Benchmark: Virtual vs CRTP
#include <chrono>
#include <iostream>
#include <vector>
// Virtual function approach
class VirtualShape {
public:
virtual double area() const = 0;
virtual ~VirtualShape() = default;
};
class VirtualCircle : public VirtualShape {
public:
VirtualCircle(double r) : radius(r) {}
double area() const override {
return 3.14159 * radius * radius;
}
private:
double radius;
};
// CRTP approach
template<typename Derived>
class CRTPShape {
public:
double area() const {
return static_cast<const Derived*>(this)->areaImpl();
}
};
class CRTPCircle : public CRTPShape<CRTPCircle> {
public:
CRTPCircle(double r) : radius(r) {}
double areaImpl() const {
return 3.14159 * radius * radius;
}
private:
double radius;
};
// Benchmark
int main() {
constexpr int N = 10000000;
// Virtual function
std::vector<VirtualShape*> virtual_shapes;
for (int i = 0; i < 100; ++i) {
virtual_shapes.push_back(new VirtualCircle(i));
}
auto start = std::chrono::steady_clock::now();
double sum = 0;
for (int i = 0; i < N; ++i) {
sum += virtual_shapes[i % 100]->area();
}
auto elapsed_virtual = std::chrono::steady_clock::now() - start;
// CRTP
std::vector<CRTPCircle> crtp_shapes;
for (int i = 0; i < 100; ++i) {
crtp_shapes.emplace_back(i);
}
start = std::chrono::steady_clock::now();
sum = 0;
for (int i = 0; i < N; ++i) {
sum += crtp_shapes[i % 100].area();
}
auto elapsed_crtp = std::chrono::steady_clock::now() - start;
std::cout << "Virtual: " << std::chrono::duration<double, std::milli>(elapsed_virtual).count() << " ms\n";
std::cout << "CRTP: " << std::chrono::duration<double, std::milli>(elapsed_crtp).count() << " ms\n";
std::cout << "Speedup: " << (elapsed_virtual.count() / (double)elapsed_crtp.count()) << "x\n";
// Cleanup
for (auto* s : virtual_shapes) delete s;
}
Typical results (varies by compiler and optimization level):
- Virtual: ~150ms
- CRTP: ~50ms
- Speedup: ~3x
Why CRTP is faster:
- No vtable lookup: Direct function call
- Inlining: Compiler can inline the function
- Better cache locality: No pointer indirection
Summary
| Item | Description |
|---|---|
| Pattern | Base class takes Derived as template parameter |
| Mechanism | static_cast<Derived*>(this) for compile-time dispatch |
| Advantages | Zero overhead, inlining, compile-time interface enforcement |
| Disadvantages | No heterogeneous containers, more complex code |
| Use cases | Performance-critical code, static polymorphism, mixins |
FAQ
Q: When should I use CRTP instead of virtual functions?
A: Use CRTP when:
- Performance is critical (tight loops, hot paths)
- You don’t need heterogeneous containers
- All types are known at compile-time
Use virtual functions when:
- You need runtime polymorphism
- You need heterogeneous containers (
std::vector<Base*>) - Types are determined at runtime
Q: Is static_cast safe in CRTP?
A: Yes, it’s safe because the compiler guarantees that this points to a Derived object. The cast is checked at compile-time and has zero runtime cost.
Q: Can I combine CRTP with virtual functions?
A: Yes, you can use both. CRTP for compile-time polymorphism and virtual functions for runtime polymorphism.
template<typename Derived>
class CRTPBase {
public:
virtual void virtualMethod() = 0; // Runtime polymorphism
void crptMethod() { // Compile-time polymorphism
static_cast<Derived*>(this)->implementation();
}
};
Q: What’s the overhead of CRTP?
A: CRTP has zero runtime overhead compared to direct calls. The only cost is increased compile time and code size due to template instantiation.
Q: Can I use CRTP with multiple inheritance?
A: Yes, CRTP works well with multiple inheritance for mixins:
class MyClass : public Printable<MyClass>,
public Comparable<MyClass>,
public Serializable<MyClass> {
// Implement required methods
};
Related posts: C++ Templates Basics, Virtual Functions, PIMPL Pattern.
One-line summary: CRTP enables compile-time polymorphism with zero overhead by passing the derived class as a template parameter and using static_cast for dispatch.
## 같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- [C++ 템플릿 | "제네릭 프로그래밍" 초보자 가이드](/blog/cpp-template-basics/)
- [C++ 가상 함수 | "Virtual Functions" 가이드](/blog/cpp-virtual-functions/)
- [C++ Pimpl Idiom 완벽 가이드 | 구현 은닉과 컴파일 시간 단축](/blog/cpp-pimpl/)
---
---
## 이 글에서 다루는 키워드 (관련 검색어)
C++, crtp, template, polymorphism, pattern, static 등으로 검색하시면 이 글이 도움이 됩니다.