TopC++ 入門 › 応用 › CRTP

CRTP(Curiously Recurring Template Pattern)

「派生クラスが自分自身をテンプレート引数として基底に渡す」という奇妙なパターン。virtual を使わずに多態を実現し、インライン化でゼロコストに近づけられる技法です。

1. 形の暗唱

crtp_shape.cpp
template<class Derived> struct Base { void action() { static_cast<Derived*>(this)->impl(); // ↓ の impl を呼ぶ } }; struct MyClass : Base<MyClass> { // ← 自分を渡す void impl() { std::cout << "hello"; } }; MyClass{}.action(); // "hello"

Base が Derived を知っているので、static_cast<Derived*>(this)派生版の関数を呼べる。virtual 無しに多態が成立します。

2. virtual 版との比較

virtual動的
struct Shape { virtual double area() const = 0; virtual ~Shape() = default; }; struct Circle : Shape { double r; double area() const override { return 3.14*r*r; } }; // 呼び出し時に vtable を経由(関節コスト) // ただし実行時に型差し替え可能
CRTP静的
template<class D> struct Shape { double area() const { return static_cast<const D*>(this)->area_impl(); } }; struct Circle : Shape<Circle> { double r; double area_impl() const { return 3.14*r*r; } }; // コンパイル時に解決 → インライン化可能 (ゼロコスト) // ただし std::vector<Shape*> のような統一コンテナが作れない

3. 比較表

virtual(動的)CRTP(静的)
ディスパッチ時刻実行時コンパイル時
コストvtable 参照(間接呼び出し)ほぼゼロ(インライン化可)
vector で統一コンテナvector<Shape*>✗ 型が違う
ABI 安定性△(テンプレートは header に出る)
典型用途GUI / 実行時切替数値計算 / STL 内部

4. 実例 1: 共通機能をベースに集約(mixin)

派生クラスに共通の振る舞い(比較、表示、カウント等)を注入する時に便利。

counter.cpp
template<class D> struct Counted { static inline int count = 0; Counted() { ++count; } ~Counted() { --count; } }; struct User : Counted<User> {}; struct Product : Counted<Product> {}; // 各型ごとに別のカウンタ User::count; // 独立 Product::count;

5. 実例 2: Expression Template(Eigen 等)

ベクトル演算ライブラリで a + b + c を一時値を作らず最終代入時に融合する技法。Eigen, Blaze, xtensor が採用。

eigen_style.cpp
template<class D> struct VecExpr { double at(size_t i) const { return static_cast<const D&>(*this).at(i); } size_t size() const { return static_cast<const D&>(*this).size(); } }; template<class L, class R> struct Add : VecExpr<Add<L,R>> { const L& l; const R& r; double at(size_t i) const { return l.at(i) + r.at(i); } size_t size() const { return l.size(); } }; // 代入時に 1 ループで融合計算: d[i] = a[i]+b[i]+c[i]

6. よくある落とし穴

  • 派生の基底テンプレート引数ミス: struct A : Base<B> のように間違えるとコンパイルは通るが実行時クラッシュ。
  • ヘッダに全実装が出る: テンプレート故のトレードオフ。インライン化のメリットと引き換え。
  • 不完全型問題: Base 内で Derived のメンバに触れるのは Derived が完全型になった後(メンバ関数定義時)だけ。ctor では触れない。
実務では std::enable_shared_from_this<T> も CRTP の一例。shared_ptr の中から自分自身への shared_ptr を取得する機能は、T が自己型を知る必要があるため CRTP 構造になっています。