一、问题场景
场景:你有一个容器,想存"满足某个接口的任何类型",但又不希望用户继承你的基类。
传统做法是定义一个抽象基类,用户必须继承它:
class Drawable {
public:
virtual void draw() const = 0;
virtual ~Drawable() = default;
};
// 用户必须继承 Drawable
class MyCircle : public Drawable { ... };
问题:用户需要用 public 继承你的类,这是侵入式的。用户可能已经有了自己的类层次,不想再改继承。
Type Erasure 的做法:非侵入式——用户不需要继承任何东西。你提供包装器。
二、Type Erasure 的核心模式
Type Erasure 的经典实现是 外部多态(External Polymorphism):
// 1. 用 concept 定义接口
template <typename T>
concept Drawable = requires(const T& t) {
{ t.draw() } -> std::same_as<void>;
};
// 2. 类型擦除包装器(用户可以传入任何满足 Drawable 的类型)
class DrawableWrapper {
struct Concept {
virtual void draw() const = 0;
virtual ~Concept() = default;
};
template <Drawable T>
struct Model : Concept {
T obj_;
Model(T obj) : obj_(std::move(obj)) {}
void draw() const override { obj_.draw(); }
};
std::unique_ptr<Concept> pimpl_;
public:
template <Drawable T>
DrawableWrapper(T obj) : pimpl_(std::make_unique<Model<T>>(std::move(obj))) {}
void draw() const { pimpl_->draw(); }
};
用法:
struct Circle {
void draw() const { std::cout << "○"; }
};
struct Square {
void draw() const { std::cout << "□"; }
};
std::vector<DrawableWrapper> shapes;
shapes.emplace_back(Circle{});
shapes.emplace_back(Square{});
for (const auto& s : shapes) {
s.draw(); // 输出 ○□
}
用户不需要继承任何东西。只要类型有 draw() 方法,就能被 DrawableWrapper 包装。
三、Type Erasure 的底层结构
DrawableWrapper 的内存布局:
栈上:
+──────────────────────────+
| pimpl_(unique_ptr) |──→ 堆上: Model<Circle>
+──────────────────────────+ +──────────────+
| vptr │──→ vtable
| obj_ (Circle) (draw -> Circle::draw)
+──────────────+
每次调用 draw() 仍然有一次虚函数调用(通过 pimpl_ 的 vtable),但这个虚函数调用被封装在包装器内部,用户看不见。
四、Type Erasure 和虚基类的对比
| 对比 | 虚基类(侵入式) | Type Erasure(非侵入式) |
|---|---|---|
| 用户是否需要继承 | 是 | 否 |
| 性能 | 一次虚函数调用 | 一次虚函数调用(和虚基类一样) |
| 对已有类型的适配 | 不能适配已有的类 | 可以,只要类型有对应方法 |
| 实现复杂度 | 简单 | 复杂(需要自己写包装器) |