본문으로 건너뛰기
Previous
Next
C++ CRTP Pattern Complete Guide

C++ CRTP Pattern Complete Guide

C++ CRTP Pattern Complete Guide

이 글의 핵심

Master C++ CRTP for static polymorphism without virtual function overhead. Complete guide with mixin counters, comparable helpers, singleton patterns, and performance comparisons.

What is CRTP?

Compile-time polymorphism and policy design connect with Strategy and PIMPL discussions in the comprehensive pattern guide, and as FAQ mentions, creation points are often considered together with Factory. Curiously Recurring Template Pattern

  • Pass derived class as template argument to base class
  • Static polymorphism (compile time)
  • Implement polymorphism without virtual functions
// Base class
template<typename Derived>
class Base {
public:
    void interface() {
        static_cast<Derived*>(this)->implementation();
    }
};
// Derived class
class Derived : public Base<Derived> {
public:
    void implementation() {
        cout << "Derived implementation" << endl;
    }
};
int main() {
    Derived d;
    d.interface();  // "Derived implementation"
}

Virtual Functions vs CRTP

Virtual Functions (Dynamic Polymorphism)

class Base {
public:
    virtual void func() = 0;
    virtual ~Base() {}
};
class Derived : public Base {
public:
    void func() override {
        cout << "Derived" << endl;
    }
};
// Runtime overhead (vtable)

CRTP (Static Polymorphism)

template<typename Derived>
class Base {
public:
    void func() {
        static_cast<Derived*>(this)->funcImpl();
    }
};
class Derived : public Base<Derived> {
public:
    void funcImpl() {
        cout << "Derived" << endl;
    }
};
// Compile time, no overhead

Practical Examples

Example 1: Counter Mixin

template<typename Derived>
class Countable {
private:
    static int count;
    
public:
    Countable() { count++; }
    Countable(const Countable&) { count++; }
    ~Countable() { count--; }
    
    static int getCount() { return count; }
};
template<typename Derived>
int Countable<Derived>::count = 0;
class Widget : public Countable<Widget> {
public:
    Widget() { cout << "Widget created" << endl; }
};
class Gadget : public Countable<Gadget> {
public:
    Gadget() { cout << "Gadget created" << endl; }
};
int main() {
    Widget w1, w2;
    Gadget g1;
    
    cout << "Widget count: " << Widget::getCount() << endl;  // 2
    cout << "Gadget count: " << Gadget::getCount() << endl;  // 1
}

Example 2: Auto-Generate Comparison Operators

template<typename Derived>
class Comparable {
public:
    friend bool operator!=(const Derived& lhs, const Derived& rhs) {
        return !(lhs == rhs);
    }
    
    friend bool operator>(const Derived& lhs, const Derived& rhs) {
        return rhs < lhs;
    }
    
    friend bool operator<=(const Derived& lhs, const Derived& rhs) {
        return !(rhs < lhs);
    }
    
    friend bool operator>=(const Derived& lhs, const Derived& rhs) {
        return !(lhs < rhs);
    }
};
class Point : public Comparable<Point> {
public:
    int x, y;
    
    Point(int x, int y) : x(x), y(y) {}
    
    // Only implement == and <, rest auto-generated
    friend bool operator==(const Point& lhs, const Point& rhs) {
        return lhs.x == rhs.x && lhs.y == rhs.y;
    }
    
    friend bool operator<(const Point& lhs, const Point& rhs) {
        if (lhs.x != rhs.x) return lhs.x < rhs.x;
        return lhs.y < rhs.y;
    }
};
int main() {
    Point p1(1, 2);
    Point p2(3, 4);
    
    cout << (p1 == p2) << endl;  // 0
    cout << (p1 != p2) << endl;  // 1
    cout << (p1 < p2) << endl;   // 1
    cout << (p1 >= p2) << endl;  // 0
}

Example 3: Singleton Mixin

template<typename Derived>
class Singleton {
protected:
    Singleton() {}
    
public:
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    
    static Derived& getInstance() {
        static Derived instance;
        return instance;
    }
};
class Config : public Singleton<Config> {
    friend class Singleton<Config>;
    
private:
    Config() { cout << "Config created" << endl; }
    int value = 0;
    
public:
    void setValue(int v) { value = v; }
    int getValue() { return value; }
};
class Logger : public Singleton<Logger> {
    friend class Singleton<Logger>;
    
private:
    Logger() { cout << "Logger created" << endl; }
    
public:
    void log(const string& msg) {
        cout << "[LOG] " << msg << endl;
    }
};
int main() {
    Config::getInstance().setValue(100);
    Logger::getInstance().log("System started");
    
    cout << Config::getInstance().getValue() << endl;  // 100
}

Example 4: Chaining Interface

template<typename Derived>
class Chainable {
protected:
    Derived& self() {
        return static_cast<Derived&>(*this);
    }
};
class QueryBuilder : public Chainable<QueryBuilder> {
private:
    string query;
    
public:
    QueryBuilder& select(const string& fields) {
        query = "SELECT " + fields;
        return self();
    }
    
    QueryBuilder& from(const string& table) {
        query += " FROM " + table;
        return self();
    }
    
    QueryBuilder& where(const string& condition) {
        query += " WHERE " + condition;
        return self();
    }
    
    string build() {
        return query;
    }
};
int main() {
    QueryBuilder qb;
    string sql = qb.select("*")
                   .from("users")
                   .where("age > 18")
                   .build();
    
    cout << sql << endl;
    // SELECT * FROM users WHERE age > 18
}

Performance Comparison

#include <chrono>
// Virtual functions
class VirtualBase {
public:
    virtual int compute(int x) = 0;
    virtual ~VirtualBase() {}
};
class VirtualDerived : public VirtualBase {
public:
    int compute(int x) override {
        return x * 2;
    }
};
// CRTP
template<typename Derived>
class CRTPBase {
public:
    int compute(int x) {
        return static_cast<Derived*>(this)->computeImpl(x);
    }
};
class CRTPDerived : public CRTPBase<CRTPDerived> {
public:
    int computeImpl(int x) {
        return x * 2;
    }
};
int main() {
    const int N = 100000000;
    
    // Virtual functions
    VirtualBase* vb = new VirtualDerived();
    auto start = chrono::high_resolution_clock::now();
    for (int i = 0; i < N; i++) {
        vb->compute(i);
    }
    auto end = chrono::high_resolution_clock::now();
    cout << "Virtual: " << chrono::duration_cast<chrono::milliseconds>(end - start).count() << "ms" << endl;
    
    // CRTP
    CRTPDerived cd;
    start = chrono::high_resolution_clock::now();
    for (int i = 0; i < N; i++) {
        cd.compute(i);
    }
    end = chrono::high_resolution_clock::now();
    cout << "CRTP: " << chrono::duration_cast<chrono::milliseconds>(end - start).count() << "ms" << endl;
}

Typical results: CRTP is 2-5x faster than virtual functions in tight loops due to inlining and no vtable lookup.

Common Issues

Issue 1: Wrong casting

// ❌ Dangerous
template<typename Derived>
class Base {
public:
    void func() {
        static_cast<Derived*>(this)->impl();
    }
};
class Wrong : public Base<OtherClass> {  // Wrong type!
public:
    void impl() {}
};
// ✅ Correct usage
class Correct : public Base<Correct> {
public:
    void impl() {}
};

Issue 2: Circular dependency

// ❌ Circular dependency
class A : public Base<B> {};  // B not yet defined
class B : public Base<A> {};
// ✅ Each uses itself as template argument
class A : public Base<A> {};
class B : public Base<B> {};

Issue 3: Missing virtual destructor

// ❌ Possible memory leak
template<typename Derived>
class Base {
    // No virtual destructor
};
// ✅ Add virtual destructor (for polymorphic deletion)
template<typename Derived>
class Base {
public:
    virtual ~Base() = default;
};

CRTP Use Scenarios

1. Performance-critical cases

// Game engines, high-performance computing
template<typename Derived>
class Entity {
public:
    void update(float dt) {
        static_cast<Derived*>(this)->updateImpl(dt);
    }
};

2. Code reuse

// Common functionality as mixin
template<typename Derived>
class Serializable {
public:
    string serialize() {
        // Serialization logic
    }
};

3. Compile-time polymorphism

// Polymorphism via template argument
template<typename T>
void process(T& obj) {
    obj.compute();  // Decided at compile time
}

FAQ

Q1: When to use CRTP?

A:

  • Performance-critical cases
  • Compile-time polymorphism needed
  • Mixin patterns

Q2: Always CRTP instead of virtual functions?

A: No. Use virtual functions when runtime polymorphism needed.

Q3: CRTP disadvantages?

A:

  • Increased code complexity
  • Longer compile times
  • No runtime polymorphism

Q4: What are mixins?

A: Pattern combining multiple base classes to add functionality.

Q5: CRTP vs Template Method pattern?

A: CRTP is static, Template Method is dynamic.

Q6: CRTP learning resources?

A:

  • “Modern C++ Design” (Andrei Alexandrescu)
  • “C++ Templates: The Complete Guide”
  • Boost library source code

Keywords

C++, CRTP, static polymorphism, templates, mixins, compile-time optimization, design patterns


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

C++, CRTP, templates, polymorphism, design patterns, metaprogramming 등으로 검색하시면 이 글이 도움이 됩니다.