std::function需要类型擦除以统一存储大小和布局各异的可调用对象;通过基类虚函数、模板派生类和指针管理实现运行时多态调用,辅以sbo优化与严格内存对齐。

std::function 为什么需要类型擦除
因为 std::function 要能存任意可调用对象(函数指针、lambda、bind 表达式、成员函数指针等),而它们的类型各不相同,大小不一,没法用统一字段直接存储。类型擦除就是把具体类型“藏起来”,只暴露统一接口,靠运行时多态或函数指针跳转来间接调用。
手写简易版的关键三步:基类 + 模板派生 + 统一指针管理
核心不是“泛型容器”,而是“统一调用入口 + 类型自持”。典型做法是定义一个抽象基类,带虚函数 invoke() 和 destroy();每个具体可调用类型都派生一个模板子类,把捕获值/函数指针存为成员,并实现虚函数;外部用 std::unique_ptr 或裸指针持有基类指针。
- 基类不能有模板参数,但虚函数必须接受统一签名(比如
void* args或固定参数包)——实际std::function用的是完美转发 + 可变参数模板,在手写简化版里常退化为void*+ 强制转换 - 构造时必须做类型擦除:用模板构造函数接收任意
F,new 出对应派生实例,把Fmove 进去;别忘了处理拷贝——要么禁止(像早期std::function),要么深拷贝派生对象(需在基类加clone()虚函数) - 调用时不能直接
obj(),必须走base_ptr->invoke(...),参数要打包成void*数组或用std::tuple封装,再在派生类里解包——这一步最容易出 ABI 错误或未定义行为
为什么不用 void* 直接存函数指针?
因为 lambda(尤其带捕获)、std::bind、成员函数指针都不是纯函数指针:void (*)() 无法容纳捕获上下文。你看到的 sizeof(std::function<void>)</void> 通常是 16 或 24 字节,前半段存函数指针,后半段存 void* 形式的“上下文指针”——手写时若硬塞进单个 void*,遇到移动语义或栈对象捕获就会悬垂。
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
- 正确做法是让每个派生类自己管理其可调用体的生命周期(堆分配 or SBO)
- SBO(small buffer optimization)是
std::function实际用的优化:小对象(如无捕获 lambda)直接存在对象内部,避免 new;手写时若不做,性能会明显差一截 - 别忽略对齐:
alignas必须和最大可能类型的对齐一致,否则placement new到内部缓冲区会崩
std::function 的 operator= 和移动语义怎么模拟
赋值操作本质是“先清理旧资源,再擦除新类型”。关键点不在语法,而在资源管理策略是否一致:如果用了堆分配,operator= 必须深拷贝整个派生对象;如果支持移动,就要在基类加 move_to() 虚函数,让派生类把资源转移到目标内存位置。
- 移动构造函数里不能简单
other.ptr = nullptr,因为other可能是 SBO 状态——此时要调用其destroy()并标记为空 -
swap()是安全兜底:交换两个对象的内部指针或缓冲区状态,比移动更易写对 - 最常漏掉的是异常安全:如果
new失败,原对象必须保持有效——这意味着operator=应该先 new,成功后再destroy()旧资源
类型擦除真正难的不是虚函数表,而是内存布局一致性、移动语义与 SBO 的耦合、以及所有路径上的异常边界——写到一半发现 std::function<void></void> 和 std::function<void int></void> 需要不同调用签名时,才真正踩进标准库没明说的坑里。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!










