Menu
Coddy logo textTech

CRTPによるミックスイン

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つのミックスインを作成します。

コードは以下の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 クラスを作成します。名前とレベルを保持し、"Player: [name] (Level [level])" という形式の文字列を返す getDescription() メソッドを提供する必要があります。

    同様に両方のミックスインを継承する Enemy クラスを作成します。タイプと体力を保持し、"Enemy: [type] with [health] HP" を返す getDescription() メソッドを提供する必要があります。

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

  • 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ミックスインが仮想関数を使用せずに、関連のないクラスに機能(カウントと記述)を追加する方法を示しています。PlayerEnemy の両方は、同じミックスインテンプレートを継承することで同じ機能を得ますが、テンプレートが異なる型でインスタンス化されるため、それぞれが独自の個別のインスタンスカウンタを維持します。

チートシート

Curiously Recurring Template Pattern (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 Mixin は、深い継承階層を必要とせずに再利用可能な機能を提供します。

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; }
};

主な利点:

  • すべてのメソッド呼び出しがコンパイル時に解決される
  • 仮想関数のオーバーヘッドがない
  • 複数の Mixin を組み合わせて機能を構成できる
  • 各テンプレートのインスタンス化が個別の状態を保持する(例:派生型ごとの個別の静的カウンタ)

自分で試してみよう

#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: player に対して describe() を呼び出す
    
    // TODO: enemy に対して describe() を呼び出す
    
    // TODO: 名前が "Guest"、レベルが 1 の 2 人目のプレイヤーを作成する
    
    // TODO: 再度 "Player count: [count]" を出力する
    
    // TODO: 2 人目のプレイヤーに対して describe() を呼び出す
    
    return 0;
}
quiz icon腕試し

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

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