c++ type erasure技巧 c++如何实现std::any和std::function

4次阅读

<p>std::any 通过 void* 加函数表实现类型擦除,不依赖虚函数;其核心是构造 / 析构 / 拷贝 / 类型查询函数指针组成的结构体,配合小对象优化与严格生命周期管理。</p>

c++ type erasure 技巧 c++ 如何实现 std::any 和 std::function

std::any 的核心是 void* + 类型擦除函数表

它不是靠虚函数继承实现的,而是用一个 struct 存函数指针:构造、析构、拷贝、类型查询。所有操作都通过这个“函数表”跳转,std::any 本身只管存 void* 和这个表指针。

实操建议:

  • 自己写简易版时,别直接裸存 void*,一定要配对管理生命周期——比如用 new 分配的,就得在析构函数里 delete;否则一 move 或赋值就悬空
  • 类型查询靠 std::type_info,但注意:不同编译单元的同名类(尤其模板实例化)可能生成不同 std::type_info 对象,typeid(T) == typeid(U) 不一定稳,优先用 std::any_cast 的运行时检查
  • 小对象优化(SOO)很关键:像 libstdc++ 和 libc++ 都给 std::any 预留了约 24–32 字节内联空间,避免小类型(如 intstd::string_view)堆分配;自己实现时漏掉这点,性能会差一截

std::function 的类型擦除比 std::any 更重

std::function 不仅要擦除目标类型,还要统一调用签名——所有可调用体(函数指针、lambda、std::bind、仿函数)都被转换成“能响应 operator() 并匹配指定参数 / 返回类型的黑盒”。

常见错误现象:

立即学习 C++ 免费学习笔记(深入)”;

  • 捕获局部变量的 lambda 赋给 std::function 后,原作用域结束,调用时报 segmentation fault——本质是擦除后没做所有权转移,得确保捕获的内容生命周期 ≥ std::function 本身
  • std::function<void></void> 接收返回 int 的函数,编译失败:擦除前就做签名匹配,不兼容的 callable 根本进不来,不是运行时报错
  • 移动后调用已移动的 std::function,触发 std::bad_function_call——因为擦除函数表里“调用函数指针”被置为 nullptr,但这个状态不会自动清零其他字段,内存未必全脏,所以不能靠 memcmp 判空

用 std::any_storage 和 std::function 的 vtable 手写简易版

没必要从零造轮子,但理解它们怎么组织数据,能避开很多误用。

实操建议:

  • 定义擦除结构体时,把函数指针打包进一个 struct,比如:
    struct any_vtable {void (*dtor)(void*);     void (*copy)(void*, const void*);     const std::type_info& (*type)(void*); };

    别把每个函数单独存为成员变量,缓存不友好,也难扩展

  • lambda 擦除进 std::function 时,编译器会生成一个唯一闭包类型,并把捕获数据作为该类型的成员;若捕获为空,有些实现(如 MSVC)会直接退化为函数指针,但别依赖——sizeof(std::function<int>)</int> 在不同 STL 实现里可能差 8~16 字节
  • 别在 std::any 里存多态对象(带虚函数的类),除非你手动保证虚表指针有效;更安全的做法是存智能指针或值语义类型

std::any_cast 和 std::function::target 的陷阱

这两个接口看着像“取出来”,实际都有隐含约束和未定义行为风险。

使用场景与坑点:

  • std::any_cast<t>(&a)</t> 返回 T*,如果 a 里不是 T 类型,返回空指针——但很多人写成 *std::any_cast<t>(&a)</t>,一解引用就崩;应该先判空再用
  • std::function::target<t>()</t> 只能取回原始类型,不能跨继承体系;比如存的是 std::unique_ptr<base>,用 target<:unique_ptr>></:unique_ptr> 拿不到,哪怕 Derived 继承 Base
  • std::functiontarget_type() 返回 std::type_info,但 C++17 起禁止用 == 直接比较两个 std::type_info 对象(标准说“行为未定义”),正确方式是用 std::type_info::hash_code()std::type_info::name()(后者不跨平台)

类型擦除不是魔法,它把编译期信息搬到运行期管理,代价是间接跳转、额外内存、以及所有生命周期必须人工对齐。最常被忽略的,其实是那个“谁负责释放”的契约——擦除层不管,得你自己盯紧。

星耀云
版权声明:本站原创文章,由 星耀云 2026-03-18发表,共计1909字。
转载说明:转载本网站任何内容,请按照转载方式正确书写本站原文地址。本站提供的一切软件、教程和内容信息仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。
text=ZqhQzanResources