型消去(Type Erasure)
std::function には関数でもラムダでもファンクタでも代入できるのに、共通のベースクラスを継承してなどいません。この魔法が型消去です。同じ「インターフェースを満たす」型を後付けで一つの型に束ねる技法を解説します。
1. なぜ必要か — virtual と比べる
C++ で多態を実現する方法は従来 2 つ:
- virtual 継承: 共通ベースを書く必要。既存型(int や std::string)に後付け不可。
- テンプレート: 同じコンテナに異種型を混在不可(vector<int> と vector<double> は別物)。
第 3 の道が型消去: 外側は共通インターフェース、内側は元の型をそのまま保持。std::vector<std::function> に関数ポインタもラムダも入れられるのはこれのおかげ。
2. 最短の実装 — AnyShape
area() を持つものなら何でも入る AnyShape を作ります。Concept → Model → Handle の 3 層構造が型消去の定番。
any_shape.cpp
// ===== 1. Concept(内部の仮想インターフェース)=====
struct ShapeConcept {
virtual double area() const = 0;
virtual std::unique_ptr<ShapeConcept> clone() const = 0;
virtual ~ShapeConcept() = default;
};
// ===== 2. Model(各型のラッパ)=====
template<class T>
struct ShapeModel : ShapeConcept {
T val;
ShapeModel(T v) : val(std::move(v)) {}
double area() const override { return val.area(); } // ← ダック型
std::unique_ptr<ShapeConcept> clone() const override {
return std::make_unique<ShapeModel>(*this);
}
};
// ===== 3. Handle(ユーザが触るクラス)=====
class AnyShape {
std::unique_ptr<ShapeConcept> p_;
public:
template<class T>
AnyShape(T v) : p_(std::make_unique<ShapeModel<T>>(std::move(v))) {}
AnyShape(const AnyShape& o) : p_(o.p_->clone()) {}
AnyShape(AnyShape&&) noexcept = default;
double area() const { return p_->area(); }
};
// ===== 使い方 =====
struct Circle { double r; double area() const { return 3.14*r*r; } };
struct Square { double s; double area() const { return s*s; } };
std::vector<AnyShape> v;
v.emplace_back(Circle{3});
v.emplace_back(Square{5});
for (auto& s : v) std::cout << s.area() << "\n";
// Circle / Square はいかなる共通ベースも継承していない!
ポイント:
- Circle/Square は何も継承していない(非侵入的)
- AnyShape 内部でコンパイル時に ShapeModel<Circle> が作られる
- 内側で virtual を使うが、外側は普通の値型
3. 標準ライブラリの中身
| 標準型 | 型消去するインターフェース |
std::function<R(Args...)> | R operator()(Args...) |
std::any | 任意型 + typeid + クローン |
std::shared_ptr<T> のカスタムデリータ | delete 相当の呼び出し |
range-v3 / std::ranges::any_view | イテレータ対 |
4. virtual / template / 型消去の比較
| virtual | template | 型消去 |
| 侵入的? | ◎ 継承必要 | ✗ 不要 | ✗ 不要 |
| 実行時多態 | ◎ | ✗ | ◎ |
| 既存型を取り込める | ✗ | ◎ | ◎ |
| 統一コンテナ | ◎ vector<Base*> | ✗ | ◎ vector<AnyShape> |
| ランタイムコスト | 間接呼び出し | ほぼ 0 | 間接呼び出し + heap |
5. 実務での判断基準
- 既存の外部型を統一したい → 型消去(侵入不要)
- 速度が最優先、型リストが静的 → variant + visit
- 拡張性が最優先(サードパーティ型も混ぜたい) → 型消去
- 自社内の全型を触れる → virtual 継承が単純で十分
性能注意: 型消去は Small Buffer Optimization をしなければ毎回ヒープ確保。std::function は 2 ポインタ分くらい内蔵バッファを持っているので小さいラムダは SBO。自作時にも同じ最適化をするなら union で内蔵バッファを用意。