C++ 专家(21):类型擦除——any、function、dyno、vtable 原理
更新时间:2026-09-01。本文是
languages/cpp/expert/专家层第 21 篇,接 编译期计算。类型擦除(type erasure)是一种多态策略:在运行时隐藏类型,但保留接口。C++ 标准库中的std::any、std::function都是类型擦除的例子。理解类型擦除的原理,才能写出灵活的库接口。
本文要回答的问题
- 类型擦除是什么?和虚函数、模板有什么区别?
- std::any 是什么?和 void* 有什么不同?
- 类型擦除的底层实现:怎么手动实现一个 type erasure?
- SBO(Small Buffer Optimization)是什么?
一、类型擦除的三种方案
cpp
// 1. 虚函数:侵入式,类型必须继承基类
struct Drawable {
virtual void draw() const = 0;
};
struct Circle : Drawable { /* ... */ };
// 2. 模板:编译期多态,不同类型生成不同代码
template<typename T>
void draw(const T& obj) { obj.draw(); }
// 3. 类型擦除:非侵入式,运行时多态,不要求继承
// 如 std::function、std::any二、std::any
cpp
#include <any>
// any:可以存储任意类型
std::any a = 42;
a = std::string("hello");
a = 3.14;
// 安全读取
if (a.has_value()) {
try {
double d = std::any_cast<double>(a); // OK
} catch (const std::bad_any_cast& e) {
// 类型不匹配
}
}
// 检查类型
if (a.type() == typeid(double)) {
double d = std::any_cast<double>(a);
}和 void 的区别:*
void*:不管理生命周期,需要手动 delete,不安全std::any:管理生命周期,自动析构,类型安全,可以检查类型
三、std::function
cpp
#include <functional>
// function:可以存储任意可调用对象
std::function<int(int, int)> func;
// 普通函数
func = std::plus<int>{};
// lambda
func = [](int a, int b) { return a * b; };
// 函数对象
struct Add {
int operator()(int a, int b) const { return a + b; }
};
func = Add{};
// 调用
int result = func(1, 2);四、手动实现类型擦除
cpp
// 需求:存储任意"可绘制"的对象,但不要求继承 Drawable 接口
// 非侵入式类型擦除
// 1. 概念接口
class Drawable {
struct Concept {
virtual void draw(std::ostream& os) const = 0;
virtual std::unique_ptr<Concept> clone() const = 0;
virtual ~Concept() = default;
};
template<typename T>
struct Model : Concept {
T obj;
Model(T obj) : obj(std::move(obj)) {}
void draw(std::ostream& os) const override {
obj.draw(os); // 要求 T 有 draw 方法
}
std::unique_ptr<Concept> clone() const override {
return std::make_unique<Model>(*this);
}
};
std::unique_ptr<Concept> pimpl;
public:
template<typename T>
Drawable(T obj) : pimpl(std::make_unique<Model<T>>(std::move(obj))) {}
Drawable(const Drawable& other) : pimpl(other.pimpl->clone()) {}
Drawable& operator=(const Drawable& other) {
if (this != &other) {
pimpl = other.pimpl->clone();
}
return *this;
}
Drawable(Drawable&&) = default;
Drawable& operator=(Drawable&&) = default;
friend std::ostream& operator<<(std::ostream& os, const Drawable& d) {
d.pimpl->draw(os);
return os;
}
};
// 使用
struct Circle {
void draw(std::ostream& os) const { os << "Circle"; }
};
struct Square {
void draw(std::ostream& os) const { os << "Square"; }
};
std::vector<Drawable> shapes;
shapes.emplace_back(Circle{});
shapes.emplace_back(Square{});
for (const auto& shape : shapes) {
std::cout << shape << "\n";
}五、SBO(Small Buffer Optimization)
cpp
// 小对象优化:小对象直接存储在栈上,不需要堆分配
// std::function、std::any 都用了 SBO
// 模拟 SBO
class SmallFunc {
static constexpr size_t BufferSize = 32;
struct Concept {
virtual void call() = 0;
virtual ~Concept() = default;
};
// 小对象直接存储在 buffer 中
alignas(std::max_align_t) char buffer[BufferSize];
Concept* ptr;
template<typename T>
void init(T&& obj) {
if constexpr (sizeof(T) <= BufferSize && alignof(T) <= alignof(std::max_align_t)) {
// 小对象:在 buffer 中构造
T* p = new (buffer) T(std::forward<T>(obj));
// 实际中需要更复杂的 vtable 管理
} else {
// 大对象:堆分配
// ptr = new Model<T>(std::forward<T>(obj));
}
}
};六、常见坑对照
| 坑 | 现象 | 对策 |
|---|---|---|
| any_cast 类型安全 | 不匹配抛出异常 | 用 try/catch 或检查 type() |
| function 性能开销 | 动态分配 + 虚函数调用 | 小对象用 lambda 捕获,避免大对象 |
| 类型擦除的拷贝 | 需要 clone 方法 | 实现 clone 或禁用拷贝 |
| 类型擦除和模板选择 | 编译期多态 vs 运行时多态 | 需要运行时多态用类型擦除 |
相关与延伸
下一篇:Pimpl 惯用法——编译防火墙、d-pointer、Qt 的实践;C++ 虚函数,见 虚函数机制。
一句话总结
C++ 类型擦除:非侵入式运行时多态,不要求继承基类;std::any 存储任意类型(类型安全),std::function 存储任意可调用对象;手动实现类型擦除用 Concept(虚函数基类)+ Model(模板派生类)模式;SBO(小对象优化)把小对象存储在栈上 buffer 中避免堆分配;类型擦除相比虚函数更灵活,相比模板有运行时开销。