CRTP — Curiously Recurring Template Pattern
CRTP(Curiously Recurring Template Pattern)는 기반 클래스가 파생 클래스를 템플릿 인수로 받는 패턴입니다. 가상 함수(vtable) 없이 컴파일 타임 다형성을 구현해 런타임 오버헤드를 제거합니다.
1. 기본 구조
섹션 제목: “1. 기본 구조”// 기반 클래스가 파생 클래스를 템플릿 인수로 받음template<typename Derived>class Base{public: void interface() { // Derived의 구현을 정적으로 호출 static_cast<Derived*>(this)->implementation(); }
// 기본 구현 (오버라이드 가능) void implementation() { std::cout << "Base 기본 구현\n"; }};
class ConcreteA : public Base<ConcreteA>{public: void implementation() { std::cout << "ConcreteA 구현\n"; }};
class ConcreteB : public Base<ConcreteB>{ // implementation() 미정의 → Base 기본 구현 사용};
int main(){ ConcreteA a; a.interface(); // "ConcreteA 구현" — 가상 함수 없음
ConcreteB b; b.interface(); // "Base 기본 구현"}2. 가상 함수 vs CRTP 성능
섹션 제목: “2. 가상 함수 vs CRTP 성능”// 가상 함수 기반struct VirtualAnimal { virtual void speak() = 0; void do_speak() { speak(); } // vtable 간접 호출};
// CRTP 기반template<typename T>struct CRTPAnimal { void do_speak() { static_cast<T*>(this)->speak(); // 인라인 가능, 직접 호출 }};
struct Dog : CRTPAnimal<Dog> { void speak() { std::cout << "Woof\n"; }};CRTP는 vtable 포인터(8바이트)가 없고, 호출이 인라인될 수 있어 성능 민감 코드에 유리합니다.
3. 믹스인 — 기능 주입
섹션 제목: “3. 믹스인 — 기능 주입”// 비교 연산자 자동 생성 믹스인template<typename T>struct Comparable{ friend bool operator!=(const T& a, const T& b) { return !(a == b); } friend bool operator> (const T& a, const T& b) { return b < a; } friend bool operator<=(const T& a, const T& b) { return !(b < a); } friend bool operator>=(const T& a, const T& b) { return !(a < b); }};
struct Point : Comparable<Point>{ int x, y; // == 와 < 만 정의하면 나머지 4개 자동 생성 bool operator==(const Point& o) const { return x == o.x && y == o.y; } bool operator< (const Point& o) const { return x < o.x || (x == o.x && y < o.y); }};4. 인스턴스 카운터
섹션 제목: “4. 인스턴스 카운터”template<typename T>class Counter{ static inline int count_ = 0;public: Counter() { ++count_; } ~Counter() { --count_; } static int count() { return count_; }};
class Widget : public Counter<Widget> {};class Button : public Counter<Button> {};
Widget w1, w2;Button b1;
std::cout << Widget::count(); // 2std::cout << Button::count(); // 1// 각 파생 클래스마다 독립된 카운터5. 정책 기반 설계와 결합
섹션 제목: “5. 정책 기반 설계와 결합”template<typename Derived, typename LogPolicy>class Service : public LogPolicy{public: void execute() { LogPolicy::log("실행 시작"); static_cast<Derived*>(this)->run(); LogPolicy::log("실행 완료"); }};
struct ConsoleLog { void log(const char* msg) { std::cout << msg << '\n'; }};
struct FileLog { void log(const char* msg) { /* 파일 기록 */ }};
class DataService : public Service<DataService, ConsoleLog>{public: void run() { /* 데이터 처리 */ }};6. C++23 대안 — Deducing this
섹션 제목: “6. C++23 대안 — Deducing this”C++23에서는 this 매개변수를 명시적으로 선언해 CRTP를 더 간결하게 대체할 수 있습니다.
struct Base { // Deducing this로 파생 타입 추론 template<typename Self> void interface(this Self& self) { self.implementation(); }
void implementation() { std::cout << "Base\n"; }};
struct Derived : Base { void implementation() { std::cout << "Derived\n"; }};
Derived d;d.interface(); // "Derived" — 템플릿 파라미터 없이 동작7. 인터페이스 강제 (Interface Enforcement)
섹션 제목: “7. 인터페이스 강제 (Interface Enforcement)”CRTP를 사용하면 파생 클래스가 특정 멤버 함수를 반드시 구현하도록 컴파일 타임에 강제할 수 있습니다.
// 파생 클래스가 반드시 구현해야 할 인터페이스 강제template<typename Derived>class Serializable{public: // 직렬화 인터페이스 — 파생 클래스가 serialize_impl을 제공해야 함 std::string serialize() const { return static_cast<const Derived*>(this)->serialize_impl(); }
static Derived deserialize(const std::string& data) { return Derived::deserialize_impl(data); }
protected: // 파생 클래스에서 구현 강제 // serialize_impl()이 없으면 링크 오류 또는 컴파일 오류 발생};
// 올바른 구현class Config : public Serializable<Config>{public: std::string key; int value;
std::string serialize_impl() const { return key + "=" + std::to_string(value); }
static Config deserialize_impl(const std::string& data) { // 파싱 로직 return Config{}; }};
Config cfg{"timeout", 30};std::cout << cfg.serialize(); // "timeout=30"8. 클론 패턴 (Covariant Clone)
섹션 제목: “8. 클론 패턴 (Covariant Clone)”가상 함수 없이 파생 타입을 반환하는 clone() 메서드 구현:
template<typename Derived>class Cloneable{public: std::unique_ptr<Derived> clone() const { return std::make_unique<Derived>( *static_cast<const Derived*>(this) ); }};
class Widget : public Cloneable<Widget>{public: int id; std::string name;
Widget(int id, std::string name) : id(id), name(std::move(name)) {}};
Widget original{1, "Button"};auto copy = original.clone(); // unique_ptr<Widget>std::cout << copy->name; // "Button"9. CRTP 함정과 주의사항
섹션 제목: “9. CRTP 함정과 주의사항”// 함정 1: 파생 클래스를 완전하지 않은 타입으로 사용 시 주의template<typename Derived>class Base{ // OK: 포인터/참조는 불완전 타입 허용 Derived* get_derived() { return static_cast<Derived*>(this); }
// 주의: sizeof(Derived)는 Derived가 완전한 타입이어야 함 // static_assert(sizeof(Derived) > 0); // 멤버 함수 내에서는 OK};
// 함정 2: 실수로 다른 파생 클래스를 템플릿 인자로 전달class Wrong : public Base<int> {}; // 의도와 다른 타입 전달 — 런타임 오류 가능
// 방어적 패턴: static_assert로 타입 검증template<typename Derived>class SafeBase{protected: SafeBase() { // 생성 시점에 Derived가 실제로 SafeBase를 상속하는지 확인 static_assert(std::is_base_of_v<SafeBase, Derived>, "Derived must inherit from SafeBase<Derived>"); }};
// 함정 3: CRTP 기반 클래스에 가상 소멸자 추가 여부// CRTP 기반 클래스는 보통 다형적 삭제를 사용하지 않으므로// 가상 소멸자가 없어도 되지만, 명확성을 위해 추가 가능template<typename Derived>class SafeClone{public: // protected 소멸자로 기반 클래스 포인터로의 직접 삭제 방지 ~SafeClone() = default; // non-virtual은 OK (CRTP는 기반 포인터로 삭제 안 함)};10. 성능 비교 — 벤치마크 관점
섹션 제목: “10. 성능 비교 — 벤치마크 관점”#include <chrono>
// 가상 함수 방식struct VirtualBase { virtual double compute(double x) = 0; virtual ~VirtualBase() = default;};struct VirtualImpl : VirtualBase { double compute(double x) override { return x * x + 1.0; }};
// CRTP 방식template<typename D>struct CRTPBase { double compute(double x) { return static_cast<D*>(this)->compute_impl(x); }};struct CRTPImpl : CRTPBase<CRTPImpl> { double compute_impl(double x) { return x * x + 1.0; }};
// CRTP는 인라인 가능하여 타이트한 루프에서 가상 함수 대비// 수 배~수십 배 빠를 수 있음 (캐시 미스, 분기 예측 실패 없음)// 단, 함수가 단순하지 않으면 차이가 줄어듦| 특성 | 가상 함수 | CRTP |
|---|---|---|
| 다형성 종류 | 런타임(동적) | 컴파일 타임(정적) |
| vtable 오버헤드 | 있음 (포인터 + 간접 호출) | 없음 |
| 인라인 최적화 | 어려움 | 가능 |
| 코드 재사용 | 상속 계층 | 믹스인 조합 |
| 이종 컨테이너 | 가능 (기반 포인터) | 불가 (타입 소거 필요) |
| C++23 대안 | — | Deducing this |
CRTP는 성능 민감한 라이브러리(STL, Eigen, Boost)에서 광범위하게 사용됩니다. 가상 함수가 필요 없는 정적 다형성, 믹스인, 카운터, 인터페이스 강제에 사용하세요. C++23 Deducing this가 컴파일러에서 지원된다면 CRTP보다 간결한 대안이 됩니다.