C++のテンプレート地獄をCRTPで突破する
*この記事はテンプレート地獄に肩まで浸かりながら、なんとか設計を楽しんでいこうというお遊び気味の内容です。実務というより「こういう世界もあるんだな〜」くらいの気持ちで読んでください。
初めに
C++のテンプレートを使っていて、こんなことを思ったことはありませんか?
- テンプレート関数を継承してぇええ!!
要するに、
virtual void add(T) = 0
って書けるといいなぁってしばしば思います。
地獄に足を踏み込んでみる
とりあえず安直にやってみますわ!
class interface {
public:
template <typename T>
virtual void foo(T) = 0;
};
main.cpp:7:3: error: 'virtual' cannot be specified on member function templates
virtual void foo(T) = 0;
^~~~~~~~
main.cpp:7:16: error: illegal initializer (only variables can be initialized)
virtual void foo(T) = 0;
なぜコンパイルエラー?
仮想関数は実行時に決まるのに、テンプレートはコンパイル時に決まるからです。
やりたいのはこれなんだよね。。
と言ってもどうしても以下のような感じで実装したいんです。
たとえば、「なんでも add できるインターフェースを作りたい」と思って、 ↓こんな感じに書きたくなる。
template <typename T>
class IAdder {
public:
virtual void add(const T& msg) = 0; // こう書きたい!
};
error: virtual function cannot be templated
😇でもできません。そんなこと。
沼にハマってみるぞ!
でもどうしてもやりたいので色々な案を考えてみます。
案: 型に合わせて増やしてみる
class IAdder {
public:
virtual ~IAdder() = default;
virtual void add(const std::string& msg) = 0;
virtual void add(int msg) = 0;
};
厳しい!!コレジャナイ!!!😂
ダメな理由:
- 型を増やすたびに関数を増やす必要あり → 保守が面倒
- 共通処理を再利用しにくい
案: void*チャレンジ
class IAdder {
public:
virtual void add(void* msg) = 0;
};
class StringAdder : public IAdder {
public:
void add(void* msg) override {
auto str = static_cast<std::string*>(msg);
std::cout << "add: " << *str << std::endl;
}
};
できるけどよ……!そんな“背徳感”と共に書くコード、保守できる自信ありますか?
void*は負けでしょ(安全じゃない)😖
問題点
- 型安全ゼロ:キャストミスったら未定義動作
- 読みづらい・テストしづらい・バグりやすい
- const安全性も死ぬ
案: std::anyを使う。
これもvoid*と変わらんでしょ!
#include <any>
class IAdder {
public:
virtual void add(std::any msg) = 0;
};
class StringAdder : public IAdder {
public:
void add(std::any msg) override {
std::cout << std::any_cast<std::string>(msg) << std::endl;
}
};
これもできるけどよ。。型消去の世界(地獄)への一歩ですね👿
型消去の話はまた今度しますわ。
その他
std::function<void(void*)>を使う案や
ダウンキャストを使う案などありますがどれもダメそうですね。
function
問題点
- 本質的には void* と変わらない
- 型安全ではなく「コールバック任せ」になる
- モックしにくく、メンテしづらい
ダウンキャスト
- 型チェックが実行時になる
- C++らしさが薄れる
- クラスが無駄に増える
ここまで来たら肩まで地獄に浸かってますね!👿
地獄から抜け出す一手👼
さてみなさんテンプレート地獄を堪能していただけたでしょうか?
実はテンプレートでとある手法をすることで抜け出すことができます。
それは CRTP です。。
CRTPとは?
CRTPは、
CRTP = Curiously Recurring Template Pattern
の略です!
奇妙な繰り返しのテンプレパターン??こわ。
「派生クラスの型を基底クラスに渡す」ことで、静的に多態性を実現するパターン
ということですよ。
CRTPを使ったコード
とりあえずお見せしましょう!
#include <iostream>
#include <string>
// CRTP ベースクラス
template <typename Derived, typename T>
class AdderBase {
public:
void add(const T& msg) {
// 派生クラスに処理を任せる
static_cast<Derived*>(this)->addImpl(msg);
}
};
class StringAdder : public AdderBase<StringAdder, std::string> {
public:
void addImpl(const std::string& msg) {
std::cout << " [StringAdder] msg = " << msg << std::endl;
}
};
class IntAdder : public AdderBase<IntAdder, int> {
public:
void addImpl(const int& value) {
std::cout << " [IntAdder] value = " << value << std::endl;
}
};
int main() {
StringAdder sa;
sa.add("hello");
IntAdder ia;
ia.add(42);
}
CRTPの何が嬉しい?
「型によってやることが違うけど、全体の流れは共通にしたい」時にCRTPは超便利になります。
例えば
template <typename Derived, typename T>
class HandlerBase {
public:
void handle(const T& input) {
logStart();
static_cast<Derived*>(this)->process(input);
logEnd();
}
private:
void logStart() { std::cout << "[Handler] Start\n"; }
void logEnd() { std::cout << "[Handler] End\n"; }
};
ここでは、logStartの後にprocessを実行していますが、
このprocessは継承先で型に依存した処理を実装するだけで十分になります。
class FloatSensorHandler : public HandlerBase<FloatSensorHandler, float> {
public:
void process(float value) {
std::cout << "[Float] value: " << value << std::endl;
}
};
class StringEventHandler : public HandlerBase<StringEventHandler, std::string> {
public:
void process(const std::string& event) {
std::cout << "[Event] type: " << event << std::endl;
}
};
ん?なんか不満そうですね!
言いたいことわかります。
継承のいいところは基底クラスとして扱って振る舞いを変えれることですよね。
継承にtemplateが必要だと型に応じて別の基底クラスとして扱われるようになります。
これに関しては簡単でさらに基底クラスを用意することで解決されます。
#include <iostream>
#include <string>
#include <vector>
class IAdder {
public:
virtual ~IAdder() = default;
virtual void call() = 0;
};
// CRTP ベースクラス
template <typename Derived, typename T>
class AdderBase : public IAdder {
public:
AdderBase(const T& value) : value_(value) {}
void call() override {
static_cast<Derived*>(this)->addImpl(value_);
}
protected:
T value_;
};
class StringAdder : public AdderBase<StringAdder, std::string> {
public:
using AdderBase::AdderBase;
void addImpl(const std::string& msg) {
std::cout << "[StringAdder] msg = " << msg << std::endl;
}
};
class IntAdder : public AdderBase<IntAdder, int> {
public:
using AdderBase::AdderBase;
void addImpl(const int& value) {
std::cout << "[IntAdder] value = " << value << std::endl;
}
};
int main() {
std::vector<std::unique_ptr<IAdder>> adders;
adders.push_back(std::make_unique<StringAdder>("hello"));
adders.push_back(std::make_unique<IntAdder>(42));
for (auto& adder : adders) {
adder->call(); // 型に応じて安全に呼ばれる!
}
}
[StringAdder] msg = hello
[IntAdder] value = 42
callにも引数を入れたいとなると現在のC++仕様では難しいので諦めましょう。
どうしても入れたいなら、
// 型ごとの共通インターフェース
template <typename T>
class IAdderTyped {
public:
virtual ~IAdderTyped() = default;
virtual void add(const T& value) = 0;
};
になります。
IAdderTypedを指定するので基底クラスは共通にはなりません。
仮に言語的に virtual void call(auto value) が書けたとしても
- どの型で渡すのかがinterfaceは知らない
- テンプレートにした瞬間 virtual と相容れない
- 一つの関数に複数の型ごとの意味が混ざってきて地獄
「call() だけで全部処理する」ような設計は、実は設計の都合を使う側に押しつける構造であって、
「安全に分離する」設計の方が圧倒的に実務で強いです。
おわりに
テンプレート地獄に片足を突っ込みながらも、型安全を捨てずに多態性を実現する。そんなバランスの良い設計ができるのがCRTPの面白さです。
「virtual と template の両立はできない」ですが、「CRTPでそれっぽくできる」
――この矛盾に立ち向かう構造は、ロガー、センサ処理、イベント分配など、実務で役立つ場面がたくさんあります。
C++の型システムと仲良く付き合っていきましょう💪
Discussion