C++ 类型擦除详解
类型擦除让 C++ 处理运行时未知类型。核心:隐藏具体类型,只暴露接口。标准库如 std::function, std::any, std::variant 都依赖它。
类型擦除是什么?
定义:运行时统一处理多种类型,编译时忘掉具体类型。只剩接口。
解决痛点:
- 模板太静态,无法动态类型(插件、网络数据)。
- 容器存异构类型。
- API 解耦:接受任意 callable。
核心机制:动态多态 + vtable。
实现原理
三件套:
- 接口:纯虚基类。
- Holder:模板派生,持值。
- 包装器:持
Base*,转发调用。
vs 模板:运行时开销(虚调用、heap),但灵活。
vs variant:variant 是静态类型擦除,编译期已知所有类型,无 heap 分配。
标准库示例
std::any (C++17)
简化实现(核心:vtable 类型擦除):
// 简化版 std::any:使用 vtable 实现类型擦除
class Any {
// VTable: 存储类型相关的操作函数指针
struct VTable {
void* (*clone)(const void*); // 拷贝函数:深拷贝对象
void (*destroy)(void*); // 析构函数:释放对象
const std::type_info& (*type)(); // 类型信息函数:获取类型ID
};
void* ptr_; // 指向堆上存储的实际对象
const VTable* vt_; // 指向类型对应的 vtable
public:
// 构造函数:接受任意类型,完美转发到堆上
template<typename T>
Any(T&& val) : ptr_(new T(std::forward<T>(val))),
vt_(get_vtable<T>()) {}
// 类型安全的转换:运行时检查类型是否匹配
template<typename T>
T* cast() {
if (vt_->type() != typeid(T)) throw std::bad_any_cast{};
return static_cast<T*>(ptr_);
}
// 析构:通过 vtable 调用正确的析构函数
~Any() { vt_->destroy(ptr_); }
};std::function (C++11)
统一 callable,核心:类型擦除 + vtable + SBO。
核心思想
问题:如何用一个类型存储任意 callable(lambda、函数指针、函数对象)?
答案:类型擦除 + 虚函数表(vtable)
┌─────────────────────────────────────────────────────────────┐
│ std::function<int(int)> f │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ vtable* ──→ [destroy][clone][call] │ │
│ │ storage ──→ 实际的 callable(类型已擦除) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘关键概念:
vtable(虚函数表):存储类型相关的操作函数指针
- 每种 callable 类型对应一个 vtable
- 编译期生成,运行时查找
SBO(Small Buffer Optimization):小对象优化
- 小对象(≤32字节)直接存在栈上,避免堆分配
- 大对象才分配到堆上
类型擦除:通过
void*或 char 数组存储,忘记具体类型
完整实现(带详细注释)
// 简化版 std::function:类型擦除 + 小对象优化(SBO)
template<typename Ret, typename... Args>
class Function {
// === 第一步:定义 vtable(虚函数表) ===
// vtable 存储类型相关的操作函数指针,每种 callable 类型对应一个 vtable
struct VTable {
void (*destroy)(void*); // 析构函数:释放 callable
void (*clone)(const void*, void*); // 拷贝函数:复制 callable
Ret (*call)(void*, Args...); // 调用函数:执行 callable
};
// === 第二步:定义存储空间 ===
// SBO: Small Buffer Optimization - 小对象优化
// 小对象(≤32字节)直接存储在栈上,避免堆分配开销
alignas(16) char buf_[32]; // 对齐到16字节,满足大多数类型的对齐要求
void* ptr_ = nullptr; // 大对象时使用:指向堆上存储的 callable
const VTable* vt_ = nullptr; // 指向类型对应的 vtable
// 编译期判断:类型F是否可以用SBO
template<typename F>
static constexpr bool CanSBO = sizeof(F) <= 32;
// === 第三步:为每种 callable 类型生成对应的 vtable ===
template<typename F>
static const VTable* get_vtable() {
static const VTable vt = {
// destroy: 调用 F 的析构函数
[](void* p) { static_cast<F*>(p)->~F(); },
// clone: 拷贝构造 F
[](const void* src, void* dst) {
new(dst) F(*static_cast<const F*>(src));
},
// call: 调用 F 的 operator()
[](void* p, Args... args) -> Ret {
return (*static_cast<F*>(p))(std::forward<Args>(args)...);
}
};
return &vt;
}
public:
// === 第四步:构造函数 - 接受任意 callable ===
template<typename F>
Function(F&& f) {
vt_ = get_vtable<F>(); // 获取类型对应的 vtable
if constexpr (CanSBO<F>) {
// SBO: 小对象直接在 buf_ 上构造,placement new
// 避免堆分配,提升性能
new(buf_) F(std::forward<F>(f));
} else {
// 大对象:分配到堆上
ptr_ = new F(std::forward<F>(f));
}
}
// === 第五步:调用操作符 - 通过 vtable 转发 ===
Ret operator()(Args... args) {
// ptr_ 非空则用 ptr_(堆上),否则用 buf_(栈上)
void* storage = ptr_ ? ptr_ : buf_;
return vt_->call(storage, std::forward<Args>(args)...);
}
// === 第六步:析构 - 根据存储位置调用正确的析构 ===
~Function() {
void* storage = ptr_ ? ptr_ : buf_;
vt_->destroy(storage);
// 如果是堆分配,需要释放内存
if (ptr_) {
delete static_cast<char*>(ptr_);
}
}
};工作流程图解
构造阶段:
┌─────────────────────────────────────────────────────────────┐
│ Function<int(int)> f = [](int x) { return x * x; }; │
│ │
│ 1. lambda 类型推断 → LambdaType │
│ 2. get_vtable<LambdaType>() → 生成 vtable │
│ ┌─────────────────────────────────────┐ │
│ │ destroy: LambdaType::~LambdaType │ │
│ │ clone: LambdaType 的拷贝构造 │ │
│ │ call: LambdaType::operator() │ │
│ └─────────────────────────────────────┘ │
│ 3. sizeof(LambdaType) ≤ 32? → 是 → SBO │
│ 4. new(buf_) LambdaType(...) → 存储在栈上 │
└─────────────────────────────────────────────────────────────┘
调用阶段:
┌─────────────────────────────────────────────────────────────┐
│ f(5) │
│ │
│ 1. 判断存储位置 → buf_(SBO) │
│ 2. vt_->call(buf_, 5) → 虚函数调用 │
│ 3. 执行 lambda → return 25 │
└─────────────────────────────────────────────────────────────┘使用示例
// 示例1:存储 lambda
Function<int(int)> f = [](int x) { return x * x; };
std::cout << f(5); // 输出: 25
// 示例2:存储函数指针
int add(int a, int b) { return a + b; }
Function<int(int, int)> g = add;
std::cout << g(3, 4); // 输出: 7
// 示例3:存储 std::bind 结果
Function<void()> h = std::bind([](int a, int b) {
std::cout << a + b << std::endl;
}, 3, 4);
h(); // 输出: 7
// 示例4:存储带捕获的 lambda(SBO)
int multiplier = 10;
Function<int(int)> square = [multiplier](int x) {
return x * x * multiplier;
};
std::cout << square(3); // 输出: 90
// 示例5:存储大对象(非 SBO,堆分配)
struct BigCallable {
std::array<int, 100> data_; // 400 字节,超过 SBO 阈值
int operator()(int x) { return data_[0] + x; }
};
Function<int(int)> big = BigCallable{};性能对比
| 场景 | SBO(小对象) | 非SBO(大对象) |
|---|---|---|
| 构造 | 栈上 placement new | 堆分配 + 构造 |
| 调用 | 虚函数调用 | 虚函数调用 |
| 析构 | 直接调用析构 | 析构 + 堆释放 |
| 内存开销 | 32字节栈空间 | 堆内存 + 指针 |
关键设计总结:
- vtable:编译期生成,运行时查找,实现类型擦除
- SBO:小对象优化,避免堆分配,提升性能
- 存储策略:
CanSBO<T>编译期判断,自动选择最优存储方式
手动实现 Any(经典模式)
// 手动实现 Any:经典类型擦除模式(虚函数版)
class Any {
// === 1. 接口层:纯虚基类,定义统一操作接口 ===
struct Concept {
virtual ~Concept() = default;
virtual Concept* clone() const = 0; // 虚拷贝,支持 Any 的拷贝构造
};
// === 2. Holder 层:模板派生类,持有具体类型 ===
template<typename T>
struct Holder : Concept {
T val_; // 实际存储的值
// 构造函数:完美转发初始化值
Holder(T&& v) : val_(std::forward<T>(v)) {}
// 虚拷贝实现:创建新的 Holder 副本
Concept* clone() const override {
return new Holder(T{val_}); // 拷贝构造 T
}
};
// === 3. 包装器层:管理 Concept 指针,对外提供统一接口 ===
std::unique_ptr<Concept> ptr_; // 用智能指针管理生命周期
public:
// 万能构造函数:接受任意类型 T
template<typename T>
Any(T&& v) : ptr_(new Holder<T>(std::forward<T>(v))) {}
// 获取存储的值:运行时类型检查
template<typename T>
T& get() {
// dynamic_cast 进行安全的向下转型
auto* h = dynamic_cast<Holder<T>*>(ptr_.get());
if (!h) throw std::bad_cast{}; // 类型不匹配
return h->val_; // 返回实际值的引用
}
};
// ===== 使用示例 =====
Any a = 42; // 存储 int
Any b = std::string("hello"); // 存储 string
Any c = a; // 拷贝构造(通过虚 clone())
std::cout << a.get<int>(); // 输出: 42
std::cout << b.get<std::string>(); // 输出: hello
// std::cout << a.get<std::string>(); // 运行时抛出 std::bad_cast优缺点
| 优点 | 缺点 |
|---|---|
| 运行时灵活(插件、事件) | 性能损失:虚调用 + heap |
| 统一接口,少模板代码 | 调试难:类型隐藏 |
| 内存占用大 |
何时用:真有动态需求。优先用模板/variant。
总结
- 类型擦除 = 虚函数 + 模板桥接
- 标准库实现:vtable + SBO 优化
- 能用模板/variant 就别用擦除



