类型擦除是值语义与虚函数结合的运行时多态技术,通过基类接口封装具体类型并由智能指针管理堆对象,核心在于隐藏真实类型、暴露统一接口。

类型擦除不是黑魔法,是值语义 + 虚函数的组合技
类型擦除的本质,是让不同类型的对象通过同一接口被统一持有和调用,而调用方完全不感知底层真实类型。它不是靠模板推导或 auto 隐藏类型,而是靠运行时多态 + 值语义封装实现的——关键在于“把具体类型藏进堆里,只暴露统一接口”。
典型做法:定义一个基类(如 concept 对应的抽象接口),再为每种实际类型生成一个派生类(常叫 model),构造时 new 出对应实例,用智能指针管理。用户看到的只是基类指针或包装类(如 std::any、std::function 的内部机制)。
- 别直接裸写虚基类+new——容易泄漏,务必配合
std::unique_ptr或引用计数 - 如果目标是零成本抽象(比如高性能容器),类型擦除反而增加虚调用开销,此时优先考虑模板参数化
-
std::any和std::function是标准库中已验证的类型擦除实现,别重复造轮子,除非有特殊约束(如无异常、无 RTTI、栈上分配)
手写简易 type-erased wrapper 时,拷贝/移动语义最容易崩
类型擦除对象必须支持拷贝(或明确禁用),否则一传参就崩溃。常见错误是只实现了基类的虚析构,却忘了在 wrapper 中重载 operator= 和拷贝构造函数,导致浅拷贝指针后 double-delete。
正确做法:基类中声明纯虚函数 clone(),每个 model 实现它返回新堆对象;wrapper 的拷贝构造函数调用该 clone(),移动构造则直接接管指针。
立即学习“C++免费学习笔记(深入)”;
- 基类析构函数必须是
virtual ~interface() = default;,否则 delete 基类指针会未定义行为 - 如果禁止拷贝(如含独占资源),就把
clone()设为= delete,并在 wrapper 中删除拷贝构造/赋值 - 移动操作不能简单
std::move(ptr)就完事——要确保原 wrapper 进入有效但空的状态(例如置空std::unique_ptr)
std::function 擦的是什么?为什么捕获 lambda 有时失效
std::function 擦除的是可调用对象的类型(函数指针、lambda、bind 表达式、仿函数类),但前提是其签名匹配。它不擦除捕获列表的生命周期——这是最常踩的坑。
例如:把局部 lambda 传给 std::function 并存储起来,lambda 捕获了局部变量的引用,等函数执行时,那些变量早已销毁。
- 捕获值(
[x])比捕获引用([&x])安全,但注意大对象拷贝开销 - 若必须捕获引用且需延长生命周期,得确保引用所指对象的生存期 ≥
std::function的生存期 - 编译器对 lambda 类型的推导是独立的,两个相同代码的 lambda 是不同类型——这正是类型擦除存在的理由
不用虚函数能做类型擦除吗?小对象优化(SOO)怎么影响行为
能,但代价是更复杂的内存管理和 SFINAE 技巧。像 std::any 在 libc++ 和 MSVC 中都用了 SOO:小对象(如 int、short lambda)直接存栈上,避免堆分配;大对象才走 heap path。这带来两个隐性影响:
- SOO 的阈值是编译器/标准库实现相关的(常见 16–32 字节),超出后性能突降,调试时可能发现“明明没变逻辑,速度却慢了十倍”
- SOO 会让 move 构造变得非平凡——小对象是 memcpy,大对象才是转移指针,测试时容易漏掉大对象分支
- 自定义类型擦除若想加 SOO,得手动管理对齐、就地缓冲区、析构标记位,非常容易出 UB
真正需要 SOO 的场景极少,先用 std::function 或 std::any 验证需求,再决定是否值得自己啃这块硬骨头。









