C++ CRTP 패턴 | "정적 다형성" 구현 가이드

C++ CRTP 패턴 | "정적 다형성" 구현 가이드

이 글의 핵심

C++ CRTP 패턴에 대한 실전 가이드입니다.

CRTP란?

컴파일 타임 다형성·정책 설계는 종합 패턴 가이드의 Strategy·PIMPL 논의와도 맞닿아 있고, FAQ에서 말한 것처럼 생성 지점은 Factory와 함께 고민하는 경우가 많습니다.

Curiously Recurring Template Pattern

  • 파생 클래스를 기본 클래스의 템플릿 인자로 전달
  • 정적 다형성 (컴파일 타임)
  • 가상 함수 없이 다형성 구현
// 기본 클래스
template<typename Derived>
class Base {
public:
    void interface() {
        static_cast<Derived*>(this)->implementation();
    }
};

// 파생 클래스
class Derived : public Base<Derived> {
public:
    void implementation() {
        cout << "Derived 구현" << endl;
    }
};

int main() {
    Derived d;
    d.interface();  // "Derived 구현"
}

가상 함수 vs CRTP

가상 함수 (동적 다형성)

class Base {
public:
    virtual void func() = 0;
    virtual ~Base() {}
};

class Derived : public Base {
public:
    void func() override {
        cout << "Derived" << endl;
    }
};

// 런타임 오버헤드 (vtable)

CRTP (정적 다형성)

template<typename Derived>
class Base {
public:
    void func() {
        static_cast<Derived*>(this)->funcImpl();
    }
};

class Derived : public Base<Derived> {
public:
    void funcImpl() {
        cout << "Derived" << endl;
    }
};

// 컴파일 타임, 오버헤드 없음

실전 예시

예시 1: 카운터 믹스인

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 생성" << endl; }
};

class Gadget : public Countable<Gadget> {
public:
    Gadget() { cout << "Gadget 생성" << endl; }
};

int main() {
    Widget w1, w2;
    Gadget g1;
    
    cout << "Widget 개수: " << Widget::getCount() << endl;  // 2
    cout << "Gadget 개수: " << Gadget::getCount() << endl;  // 1
}

예시 2: 비교 연산자 자동 생성

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) {}
    
    // ==와 <만 구현하면 나머지는 자동
    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
}

예시 3: 싱글톤 믹스인

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 생성" << 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 생성" << endl; }
    
public:
    void log(const string& msg) {
        cout << "[LOG] " << msg << endl;
    }
};

int main() {
    Config::getInstance().setValue(100);
    Logger::getInstance().log("시스템 시작");
    
    cout << Config::getInstance().getValue() << endl;  // 100
}

예시 4: 체이닝 인터페이스

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
}

성능 비교

#include <chrono>

// 가상 함수
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;
    
    // 가상 함수
    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;
}

자주 발생하는 문제

문제 1: 잘못된 캐스팅

// ❌ 위험
template<typename Derived>
class Base {
public:
    void func() {
        static_cast<Derived*>(this)->impl();
    }
};

class Wrong : public Base<OtherClass> {  // 잘못된 타입!
public:
    void impl() {}
};

// ✅ 올바른 사용
class Correct : public Base<Correct> {
public:
    void impl() {}
};

문제 2: 순환 의존

// ❌ 순환 의존
class A : public Base<B> {};  // B가 아직 정의 안됨
class B : public Base<A> {};

// ✅ 각자 자신을 템플릿 인자로
class A : public Base<A> {};
class B : public Base<B> {};

문제 3: 가상 소멸자 누락

// ❌ 메모리 누수 가능
template<typename Derived>
class Base {
    // 가상 소멸자 없음
};

// ✅ 가상 소멸자 추가 (다형적 삭제 시)
template<typename Derived>
class Base {
public:
    virtual ~Base() = default;
};

CRTP 사용 시나리오

1. 성능이 중요한 경우

// 게임 엔진, 고성능 계산
template<typename Derived>
class Entity {
public:
    void update(float dt) {
        static_cast<Derived*>(this)->updateImpl(dt);
    }
};

2. 코드 재사용

// 공통 기능을 믹스인으로
template<typename Derived>
class Serializable {
public:
    string serialize() {
        // 직렬화 로직
    }
};

3. 컴파일 타임 다형성

// 템플릿 인자로 다형성
template<typename T>
void process(T& obj) {
    obj.compute();  // 컴파일 타임에 결정
}

FAQ

Q1: CRTP는 언제 사용하나요?

A:

  • 성능이 중요한 경우
  • 컴파일 타임 다형성 필요
  • 믹스인 패턴

Q2: 가상 함수 대신 항상 CRTP?

A: 아니요. 런타임 다형성이 필요하면 가상 함수를 사용하세요.

Q3: CRTP의 단점은?

A:

  • 코드 복잡도 증가
  • 컴파일 시간 증가
  • 런타임 다형성 불가

Q4: 믹스인이란?

A: 여러 기본 클래스를 조합하여 기능을 추가하는 패턴입니다.

Q5: CRTP vs 템플릿 메서드 패턴?

A: CRTP는 정적, 템플릿 메서드는 동적입니다.

Q6: CRTP 학습 리소스는?

A:

  • “Modern C++ Design” (Andrei Alexandrescu)
  • “C++ Templates: The Complete Guide”
  • Boost 라이브러리 소스 코드

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

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

  • C++ Policy-Based Design | “정책 기반 설계” 가이드
  • C++ CRTP 완벽 가이드 | 정적 다형성과 컴파일 타임 최적화
  • C++ 템플릿 템플릿 인자 | template template parameter 가이드

관련 글

  • C++ CRTP 완벽 가이드 | 정적 다형성과 컴파일 타임 최적화
  • C++ Policy-Based Design |
  • C++ auto 타입 추론 | 복잡한 타입을 컴파일러에 맡기기
  • C++ CTAD |
  • C++20 Concepts 완벽 가이드 | 템플릿 제약의 새 시대