TopC++ 入門 › 応用 › 型消去

型消去(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 / 型消去の比較

virtualtemplate型消去
侵入的?◎ 継承必要✗ 不要✗ 不要
実行時多態
既存型を取り込める
統一コンテナ◎ vector<Base*>◎ vector<AnyShape>
ランタイムコスト間接呼び出しほぼ 0間接呼び出し + heap

5. 実務での判断基準

  • 既存の外部型を統一したい → 型消去(侵入不要)
  • 速度が最優先、型リストが静的 → variant + visit
  • 拡張性が最優先(サードパーティ型も混ぜたい) → 型消去
  • 自社内の全型を触れる → virtual 継承が単純で十分
性能注意: 型消去は Small Buffer Optimization をしなければ毎回ヒープ確保。std::function は 2 ポインタ分くらい内蔵バッファを持っているので小さいラムダは SBO。自作時にも同じ最適化をするなら union で内蔵バッファを用意。