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 構造になっています。