C++におけるCRTPの仕組みと実践的な活用例

CRTP(Curiously Recurring Template Pattern)は、C++のテンプレートプログラミングにおいて用いられる強力なイディオムの一つである。このパターンの基本的な構造は、派生クラスが基底クラスを継承する際、派生クラス自身を基底クラスのテンプレート引数として渡すというものである。

CRTPの動機と静的ポリモーフィズム

例えば、パフォーマンスが極めて重要となる数値計算ライブラリを設計する場面を考える。仮想関数を用いた動的ポリモーフィズムを実装した場合、関数呼び出し時にvtable(仮想関数テーブル)を参照するオーバーヘッドが発生する。この実行時コストは、頻繁に呼び出される演算処理において無視できない影響を及ぼす。

CRTPを用いることで、この実行時オーバーヘッドをゼロにしつつ、静的ポリモーフィズムを実現できる。以下の例では、行列クラスの基底テンプレートを定義し、派生クラスで実装された乗算演算子を利用して複合代入演算子を自動生成している。

template <typename Derived>
class MatrixBase {
public:
    Derived& self() { return static_cast<Derived&>(*this); }

    Derived& operator*=(const Derived& rhs) {
        self() = self() * rhs;
        return self();
    }
};

class Mat2f : public MatrixBase<Mat2f> {
    float data[4]{};
public:
    Mat2f() = default;
    // 乗算演算子の個別実装
    friend Mat2f operator*(const Mat2f& lhs, const Mat2f& rhs) {
        Mat2f result;
        // 乗算ロジックの実装...
        return result;
    }
};

このように、派生クラスで定義したoperator*を活用し、基底クラスでoperator*=を一度だけ実装することで、コードの重複を避けつつ実行時の無駄を排除できる。

CRTPの主な活用場面

1. 静的ポリモーフィズムの実現

動的ポリモーフィズムはオブジェクトの型が実行時に決定されるが、静的ポリモーフィズムではコンパイル時に型が確定する。CRTPを用いると、仮想関数を使わずにインターフェースの統一とオーバーライド相当の振る舞いを実現できる。

template <typename T>
class EventHandler {
public:
    void dispatch() {
        static_cast<T*>(this)->process();
    }
    void process() { std::cout << "Default process\n"; }
};

class ClickHandler : public EventHandler<ClickHandler> {
public:
    void process() { std::cout << "Click handled\n"; }
};

class KeyHandler : public EventHandler<KeyHandler> {
public:
    void process() { std::cout << "Key handled\n"; }
};

template <typename T>
void triggerEvent(T& handler) {
    handler.dispatch();
}

EventHandlerdispatchメソッドは、テンプレートパラメータであるTthisポインタをダウンキャストし、対応するprocessを呼び出す。これにより仮想関数テーブルを介さない直接的な関数呼び出しが可能となる。

2. コードの再利用と拡張性の向上

共通の処理を基底テンプレートクラスに集約することで、派生クラスの実装を簡潔に保つことができる。

template <typename T>
class Identifiable {
public:
    void printClassName() {
        std::cout << typeid(T).name() << std::endl;
    }
};

class UserEntity : public Identifiable<UserEntity> {};
class ItemEntity : public Identifiable<ItemEntity> {};

上記の通り、printClassNameを基底クラスに一度書くだけで、全ての派生クラスが自身の正しい型情報を出力できる。

3. ポリモーフィックなコピー構築(クローン生成)

オブジェクトの複製を行うcloneメソッドは、従来 each派生クラスでオーバーライドする必要があった。CRTPを用いれば、この実装を一箇所にまとめることができる。

class Node {
public:
    virtual ~Node() = default;
    virtual Node* duplicate() const = 0;
};

template <typename Derived>
class ClonableNode : public Node {
public:
    Node* duplicate() const override {
        return new Derived(static_cast<const Derived&>(*this));
    }
};

class TextNode : public ClonableNode<TextNode> {};
class ShapeNode : public ClonableNode<ShapeNode> {};

各ノードクラスはClonableNodeを継承するだけで、自動的に正しい型の複製を返すduplicateメソッドを獲得する。

4. メソッドチェーンの実装

派生クラスの型を基底クラスが知ることができるため、流暢なインターフェース(Fluent Interface)やメソッドチェーンを型安全に構築できる。

template <typename Builder>
class QueryBuilder {
protected:
    std::string query_;
public:
    Builder& addSelect(const std::string& cols) {
        query_ += "SELECT " + cols + " ";
        return static_cast<Builder&>(*this);
    }
    Builder& addFrom(const std::string& table) {
        query_ += "FROM " + table + " ";
        return static_cast<Builder&>(*this);
    }
};

class SQLBuilder : public QueryBuilder<SQLBuilder> {
public:
    SQLBuilder& where(const std::string& cond) {
        query_ += "WHERE " + cond + ";";
        return *this;
    }
    std::string build() { return query_; }
};

基底クラスのメソッドがstatic_castによって派生クラスの参照を返すようにすることで、派生クラス固有のメソッド(where)と基底クラスのメソッド(addSelectなど)を途切れることなく連鎖させることができる。

タグ: C++ CRTP テンプレートメタプログラミング 静的ポリモーフィズム

6月24日 19:54 投稿