Menu
CoddyTech

CRTPによるMixin

CoddyのC++ジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 77/104。

奇妙に再帰するテンプレートパターン(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は、ミックスイン(クラスに「混ぜ込む」ことができる再利用可能な機能)を作成する際に特に強力です。従来の継承とは異なり、ミックスインは深い階層構造を作ることなく機能を追加します。

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ミックスインを使ってさまざまなクラスに再利用可能な機能を追加するロギングシステムを作成しましょう。任意のクラスに「混ぜ込める」2つのミックスインを作成します。1つはインスタンス数を数えるもの、もう1つは文字列表現を生成するものです。

コードを次の3つのファイルに分けて構成します。

  • Mixins.h:再利用可能な機能を提供する2つのCRTPミックスインテンプレートを定義します。

    派生クラスのインスタンス数を追跡するCountableミックスインテンプレートを作成します。コンストラクターでインクリメントし、デストラクターでデクリメントする静的カウンターを持たせます。現在のカウントを返す静的メソッドgetCount()を用意します。

    describe()メソッドを提供するDescribableミックスインテンプレートを作成します。このメソッドはstatic_castを使って派生クラスにアクセスし、そのgetDescription()メソッドを呼び出してから、結果を改行の後に出力します。

    CRTPミックスインでは、コンパイル時に派生クラスのメンバーにアクセスするためにstatic_cast<Derived*>(this)を使うことを忘れないでください。

  • Entities.h:両方のミックスインを継承する2つのエンティティクラスを定義します。

    Countable<Player>とDescribable<Player>の両方を継承するPlayerクラスを作成します。名前とレベルを格納し、次の形式の文字列を返すgetDescription()メソッドを用意します:Player: [name] (Level [level])

    両方のミックスインを継承するEnemyクラスを作成します。タイプと体力を格納し、次の文字列を返すgetDescription()メソッドを用意します:Enemy: [type] with [health] HP

    各クラスの静的カウンターを初期化するのを忘れないでください。

  • main.cpp:4つの入力を読み込みます(それぞれ別の行)。
    1. プレイヤー名(文字列)
    2. プレイヤーレベル(整数)
    3. 敵のタイプ(文字列)
    4. 敵の体力(整数)

    指定された値でPlayerとEnemyを作成します。次に、ミックスインの機能を示します。

    1. 静的メソッドgetCount()を使ってPlayer count: [count]を出力します
    2. Enemy count: [count]を出力します
    3. プレイヤーに対してdescribe()を呼び出します
    4. 敵に対してdescribe()を呼び出します
    5. 名前を"Guest"、レベルを1として2人目のプレイヤーを作成します
    6. 更新後のカウントを示すため、もう一度Player count: [count]を出力します
    7. 2人目のプレイヤーに対して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ミックスインで無関係なクラスに機能(カウントと説明)を追加する方法を示します。PlayerとEnemyは同じミックスインテンプレートを継承することで同じ機能を利用できますが、テンプレートが異なる型でインスタンス化されるため、それぞれ独立したインスタンスカウンターを持ちます。

自分で試してみよう

#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: 指定された名前とレベルでPlayerを作成する
    
    // TODO: 指定されたタイプと体力でEnemyを作成する
    
    // TODO: Player::getCount()を使って"Player count: [count]"を出力する
    
    // TODO: Enemy::getCount()を使って"Enemy count: [count]"を出力する
    
    // TODO: プレイヤーのdescribe()を呼び出す
    
    // TODO: 敵のdescribe()を呼び出す
    
    // TODO: 名前が"Guest"でレベルが1の2人目のプレイヤーを作成する
    
    // TODO: "Player count: [count]"を再度出力する
    
    // TODO: 2人目のプレイヤーのdescribe()を呼び出す
    
    return 0;
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン

自分で練習してみよう: C++オンラインコンパイラ