类型擦除让 C++ 在运行时处理未知类型。核心:隐藏具体类型,只暴露接口。标准库如 std::function, std::any, std::variant 都依赖它。
类型擦除是什么?
定义:运行时统一处理多种类型,编译时忘掉具体类型。只剩接口。
解决痛点:
- 模板太静态,无法处理运行时类型(插件、网络数据)。
- 容器存异构类型。
- API 解耦:接受任意 callable。
核心机制:动态多态 + vtable。
实现原理
三件套:
- 接口:纯虚基类。
- Holder:模板派生,持值。
- 包装器:持
Base*,转发调用。
vs 模板:模板是静态分发,零运行时开销;类型擦除是动态分发,有虚调用和堆分配成本。
vs variant:std::variant 是静态类型擦除,编译期已知所有类型,无堆分配。
标准库示例
std::any (C++17)
简化实现(核心:vtable + SBO):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| class Any { struct VTable { void* (*clone)(const void* src, void* dst); void (*destroy)(void* obj); const std::type_info& (*type)(); };
alignas(std::max_align_t) char buf_[16]; void* ptr_ = nullptr; const VTable* vt_ = nullptr;
template<typename T> static const VTable* get_vtable() { static const VTable vt = { [](const void* src, void* dst) -> void* { if constexpr (sizeof(T) <= sizeof(buf_)) { return new(dst) T(*static_cast<const T*>(src)); } else { return new T(*static_cast<const T*>(src)); } }, [](void* obj) { if constexpr (sizeof(T) <= sizeof(buf_)) { static_cast<T*>(obj)->~T(); } else { delete static_cast<T*>(obj); } }, []() -> const std::type_info& { return typeid(T); } }; return &vt; }
public: template<typename T> Any(T&& val) { using DT = std::decay_t<T>; vt_ = get_vtable<DT>(); if constexpr (sizeof(DT) <= sizeof(buf_)) { ptr_ = buf_; new(ptr_) DT(std::forward<T>(val)); } else { ptr_ = new DT(std::forward<T>(val)); } }
template<typename T> T* cast() { if (vt_->type() != typeid(T)) throw std::bad_any_cast{}; return static_cast<T*>(ptr_); }
~Any() { if (vt_) vt_->destroy(ptr_); } };
|
要点:vtable 管理类型相关操作;SBO 让小对象走栈,避免堆分配。
std::function (C++11)
统一 callable,核心:类型擦除 + vtable + SBO。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| template<typename Ret, typename... Args> class Function { struct VTable { void (*destroy)(void*); void (*clone)(const void*, void*); Ret (*call)(void*, Args...); };
alignas(16) char buf_[32]; void* ptr_ = nullptr; const VTable* vt_ = nullptr;
template<typename F> static constexpr bool CanSBO = sizeof(F) <= sizeof(buf_);
template<typename F> static const VTable* get_vtable() { static const VTable vt = { [](void* p) { static_cast<F*>(p)->~F(); }, [](const void* src, void* dst) { new(dst) F(*static_cast<const F*>(src)); }, [](void* p, Args... args) -> Ret { return (*static_cast<F*>(p))(std::forward<Args>(args)...); } }; return &vt; }
public: template<typename F> Function(F&& f) { vt_ = get_vtable<F>(); if constexpr (CanSBO<F>) { new(buf_) F(std::forward<F>(f)); } else { ptr_ = new F(std::forward<F>(f)); } }
Ret operator()(Args... args) { void* storage = ptr_ ? ptr_ : buf_; return vt_->call(storage, std::forward<Args>(args)...); }
~Function() { void* storage = ptr_ ? ptr_ : buf_; vt_->destroy(storage); delete static_cast<char*>(ptr_); } };
|
调用流程:f(5) → 选 storage(buf_ / ptr_)→ vt_->call → 执行实际 callable。
其他类型擦除:std::shared_ptr/std::unique_ptr 也算简单擦除——指针类型统一,指向任意对象。
手动实现 Any(经典模式)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| class Any { struct Concept { virtual ~Concept() = default; virtual Concept* clone() const = 0; virtual const std::type_info& type() const = 0; };
template<typename T> struct Holder : Concept { T val_; Holder(T&& v) : val_(std::forward<T>(v)) {} Concept* clone() const override { return new Holder(T{val_}); } const std::type_info& type() const override { return typeid(T); } };
std::unique_ptr<Concept> ptr_;
public: template<typename T> Any(T&& v) : ptr_(new Holder<T>(std::forward<T>(v))) {}
Any(const Any& other) : ptr_(other.ptr_ ? other.ptr_->clone() : nullptr) {} Any(Any&&) noexcept = default;
template<typename T> T& get() { auto* h = dynamic_cast<Holder<T>*>(ptr_.get()); if (!h) throw std::bad_cast{}; return h->val_; } };
|
关键点:Concept 擦除类型;Holder<T> 保留值;clone() 实现深拷贝;dynamic_cast 安全取回类型。
优缺点
| 优点 | 缺点 |
|---|
| 运行时灵活(插件、事件、回调) | 性能损失:虚调用 + 堆分配 |
| 统一接口,少模板膨胀 | 调试难:类型隐藏 |
| 解耦:调用方不依赖具体类型 | 内存占用大 |
何时用:真有动态类型需求。能静态解决时,优先用模板或 std::variant。
扩展
- C++20 Concepts:可约束类型擦除的模板参数,提前拒绝不满足接口的类型。
- CRTP:在需要静态多态时替代虚函数,减少运行时开销。
- 库:标准库不够用时看 Boost.TypeErasure /
Boost.Any。
总结
- 类型擦除 = 虚函数 + 模板桥接。
- 标准库实现:vtable + SBO 优化。
- 能用模板 /
variant 就别用擦除。