Menu
Coddy logo textTech

CRTP를 통한 Mixin

Coddy C++ 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨 — 104개 중 77번째.

기이하게 반복되는 템플릿 패턴(Curiously Recurring Template Pattern, CRTP)은 클래스가 자기 자신을 템플릿 인자로 전달하면서 템플릿 기반 클래스를 상속받는 기법입니다. 이는 컴파일 타임 다형성을 가능하게 하며, 기반 클래스가 가상 함수 없이도 파생 클래스의 멤버에 접근할 수 있도록 합니다.

기본적인 CRTP 구조는 다음과 같습니다:

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

class MyClass : public Base<MyClass> {
public:
    void implementation() {
        std::cout << "MyClass implementation\n";
    }
};

CRTP는 클래스에 "혼합"될 수 있는 재사용 가능한 기능인 믹스인(mixins)을 만드는 데 특히 강력합니다. 전통적인 상속과 달리, 믹스인은 깊은 계층 구조를 생성하지 않고도 기능을 추가합니다:

template <typename Derived>
class Printable {
public:
    void print() const {
        const Derived& self = static_cast<const Derived&>(*this);
        std::cout << self.toString() << "\n";
    }
};

class Person : public Printable<Person> {
    std::string name;
public:
    Person(const std::string& n) : name(n) {}
    std::string toString() const { return "Person: " + name; }
};

// 사용법:
Person p("Alice");
p.print();  // 출력: Person: Alice

주요 장점은 모든 메서드 호출이 컴파일 타임에 결정되어 가상 함수 오버헤드가 제거된다는 것입니다. 여러 CRTP 믹스인을 결합하여 기능을 구성할 수 있으며, 타입을 컴파일 타임에 알 수 있는 경우 런타임 다형성에 대한 유연한 대안이 됩니다.

challenge icon

챌린지

쉬움

가상 함수 오버헤드 없이 서로 다른 클래스에 재사용 가능한 기능을 추가하기 위해 CRTP 믹스인(mixin)을 사용하는 로깅 시스템을 구축해 보겠습니다. 모든 클래스에 "믹스인"할 수 있는 두 가지 믹스인을 만들 것입니다. 하나는 인스턴스 수를 세는 용도이고, 다른 하나는 문자열 표현을 생성하는 용도입니다.

코드는 다음 세 개의 파일로 구성됩니다:

  • Mixins.h: 재사용 가능한 기능을 제공하는 두 개의 CRTP 믹스인 템플릿을 정의합니다.

    파생 클래스의 인스턴스가 몇 개 존재하는지 추적하는 Countable 믹스인 템플릿을 만듭니다. 생성자에서 증가하고 소멸자에서 감소하는 정적 카운터를 가져야 합니다. 현재 카운트를 반환하는 정적 메서드 getCount()를 제공하세요.

    describe() 메서드를 제공하는 Describable 믹스인 템플릿을 만듭니다. 이 메서드는 static_cast를 사용하여 파생 클래스에 접근하고 해당 클래스의 getDescription() 메서드를 호출한 다음, 결과 뒤에 개행 문자를 붙여 출력해야 합니다.

    CRTP 믹스인은 컴파일 타임에 파생 클래스의 멤버에 접근하기 위해 static_cast<Derived*>(this)를 사용한다는 점을 기억하세요.

  • Entities.h: 두 믹스인을 모두 상속받는 두 개의 엔티티 클래스를 정의합니다.

    Countable<Player>Describable<Player>를 모두 상속받는 Player 클래스를 만듭니다. 이름과 레벨을 저장해야 하며, Player: [name] (Level [level]) 형식의 문자열을 반환하는 getDescription() 메서드를 제공해야 합니다.

    마찬가지로 두 믹스인을 모두 상속받는 Enemy 클래스를 만듭니다. 타입과 체력을 저장해야 하며, Enemy: [type] with [health] HP를 반환하는 getDescription() 메서드를 제공해야 합니다.

    각 클래스에 대한 정적 카운터를 초기화하는 것을 잊지 마세요.

  • main.cpp: 네 개의 입력(각각 별도의 줄)을 읽습니다:
    1. 플레이어 이름 (문자열)
    2. 플레이어 레벨 (정수)
    3. 적 타입 (문자열)
    4. 적 체력 (정수)

    주어진 값으로 Player와 Enemy를 생성합니다. 그런 다음 믹스인을 시연합니다:

    1. 정적 getCount() 메서드를 사용하여 Player count: [count]를 출력합니다.
    2. Enemy count: [count]를 출력합니다.
    3. 플레이어에 대해 describe()를 호출합니다.
    4. 적에 대해 describe()를 호출합니다.
    5. 이름이 "Guest"이고 레벨이 1인 두 번째 플레이어를 생성합니다.
    6. 업데이트된 카운트를 보여주기 위해 Player count: [count]를 다시 출력합니다.
    7. 두 번째 플레이어에 대해 describe()를 호출합니다.

예를 들어, 입력값이 Hero, 10, Dragon, 500인 경우:

Player count: 1
Enemy count: 1
Player: Hero (Level 10)
Enemy: Dragon with 500 HP
Player count: 2
Player: Guest (Level 1)

이 챌린지는 CRTP 믹스인이 가상 함수를 사용하지 않고도 관련 없는 클래스에 기능(카운팅 및 설명)을 추가하는 방법을 보여줍니다. PlayerEnemy 모두 동일한 믹스인 템플릿을 상속받아 동일한 기능을 얻지만, 템플릿이 서로 다른 타입으로 인스턴스화되기 때문에 각자 별도의 인스턴스 카운터를 유지합니다.

치트 시트

CRTP(Curiously Recurring Template Pattern)는 클래스가 템플릿 베이스 클래스로부터 상속받으면서 자기 자신을 템플릿 인자로 전달함으로써 컴파일 타임 다형성을 가능하게 합니다:

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

class MyClass : public Base<MyClass> {
public:
    void implementation() {
        std::cout << "MyClass implementation\n";
    }
};

CRTP 믹스인(Mixins)은 깊은 상속 계층 구조 없이 재사용 가능한 기능을 제공합니다:

template <typename Derived>
class Printable {
public:
    void print() const {
        const Derived& self = static_cast<const Derived&>(*this);
        std::cout << self.toString() << "\n";
    }
};

class Person : public Printable<Person> {
    std::string name;
public:
    Person(const std::string& n) : name(n) {}
    std::string toString() const { return "Person: " + name; }
};

주요 장점:

  • 모든 메서드 호출이 컴파일 타임에 결정됨
  • 가상 함수 오버헤드가 없음
  • 여러 믹스인을 결합하여 기능을 구성할 수 있음
  • 각 템플릿 인스턴스화는 별도의 상태를 유지함 (예: 파생 타입별 별도의 정적 카운터)

직접 해보기

#include <iostream>
#include <string>
#include "Entities.h"

using namespace std;

int main() {
    // 입력 읽기
    string playerName;
    int playerLevel;
    string enemyType;
    int enemyHealth;
    
    cin >> playerName;
    cin >> playerLevel;
    cin >> enemyType;
    cin >> enemyHealth;
    
    // TODO: 주어진 name과 level로 Player 생성
    
    // TODO: 주어진 type과 health로 Enemy 생성
    
    // TODO: Player::getCount()를 사용하여 "Player count: [count]" 출력
    
    // TODO: Enemy::getCount()를 사용하여 "Enemy count: [count]" 출력
    
    // TODO: player에 대해 describe() 호출
    
    // TODO: enemy에 대해 describe() 호출
    
    // TODO: 이름이 "Guest"이고 level이 1인 두 번째 player 생성
    
    // TODO: "Player count: [count]"를 다시 출력
    
    // TODO: 두 번째 player에 대해 describe() 호출
    
    return 0;
}
quiz icon실력 점검

이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.

객체 지향 프로그래밍의 모든 레슨