콘텐츠로 이동

CRTP — Curiously Recurring Template Pattern

CRTP(Curiously Recurring Template Pattern)는 기반 클래스가 파생 클래스를 템플릿 인수로 받는 패턴입니다. 가상 함수(vtable) 없이 컴파일 타임 다형성을 구현해 런타임 오버헤드를 제거합니다.


// 기반 클래스가 파생 클래스를 템플릿 인수로 받음
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 기본 구현"
}

// 가상 함수 기반
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바이트)가 없고, 호출이 인라인될 수 있어 성능 민감 코드에 유리합니다.


// 비교 연산자 자동 생성 믹스인
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); }
};

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(); // 2
std::cout << Button::count(); // 1
// 각 파생 클래스마다 독립된 카운터

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() { /* 데이터 처리 */ }
};

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"

가상 함수 없이 파생 타입을 반환하는 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"

// 함정 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는 기반 포인터로 삭제 안 함)
};

#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보다 간결한 대안이 됩니다.