当前位置: 首页 > news >正文

function的类型擦除

要真正理解 std::function 的“类型擦除”,我们需要拆开它的黑盒子,看看编译器在底层到底生成了什么样的代码结构。

所谓的“类型擦除”(Type Erasure),本质上就是:在编译期我不关心你到底是谁(结构体?函数指针?Lambda?),但我依然要把你存起来,并且在运行期能正确调用你。

为了实现这一点,std::function 在内部使用了 “桥接模式 (Bridge Pattern)” + “虚函数多态” 的组合拳。

我们可以把 std::function 的内部结构拆解为三个核心层级。为了方便理解,我会写一个简化版的 MyFunction 来模拟标准库的实现。


第一层:统一的外壳 (The Wrapper)

这是我们在代码里直接使用的 std::function<void(int)>。它本身不是模板类(指它的类名不包含具体的 Callable 类型,只包含返回值和参数),但它的构造函数是模板。

它的内部主要持有一个基类指针

// 假设我们要模拟 std::function<void(int)>
class MyFunction {
public:// 构造函数是模板:这里是“类型擦除”发生的入口!// 无论传进来的是函数指针,还是巨大的 Struct,这里都用 F 接收template <typename F>MyFunction(F f) {// new 出具体的实现类,但赋值给基类指针// 这一步之后,ptr 就不知道 F 具体是谁了,只知道它是一个 CallableBaseptr = new CallableImpl<F>(f); }~MyFunction() { delete ptr; }// 调用操作符void operator()(int arg) {// 通过虚函数机制,调用实际的代码ptr->call(arg);}private:// 内部定义的基类指针struct CallableBase {virtual void call(int) = 0;virtual ~CallableBase() = default;};CallableBase* ptr; // <--- 关键:只持有基类指针
};

第二层:隐藏的具体实现 (The Eraser)

要让上面的 ptr->call() 工作,我们需要一个中间层。这个中间层利用 C++ 的模板类继承非模板基类的特性。

这就是为什么 std::function 内部会有虚表(V-Table)。

// 这是一个模板类,继承自非模板的基类
// 每一个不同的 F(不同的 Lambda,不同的函数指针)都会实例化出一个新的 CallableImpl 类
template <typename F>
struct CallableImpl : public CallableBase {F data; // 这里真正存储了你的对象(函数指针、Lambda 等)CallableImpl(F f) : data(f) {}// 重写虚函数void call(int arg) override {data(arg); // 调用实际存储的对象}
};

发生了什么?

  1. 当你把一个 Lambda 传给 MyFunction 时,编译器实例化了 CallableImpl<YourLambdaType>
  2. 这个 CallableImpl 知道具体类型,也知道大小。
  3. 但是,它被转型(Cast)成了 CallableBase* 存到了 ptr 里。
  4. 从此以后,外面的 MyFunction 只知道 ptr 指向一个“能调用的东西”,类型信息被“擦除”了,只留下了“行为(虚函数)”。

第三层:内存管理的魔法 (SBO - Small Buffer Optimization)

你提到的“装下 4 字节还是大对象”,涉及到 SBO (小对象优化)

如果每次创建一个 std::function 都要 new 一次(分配堆内存),那性能太差了。特别是对于只有 8 字节的函数指针。

标准库的实现通常会在类内部放一个小缓冲区(union)

内存布局示意图

class std_function_implementation {
private:// 这是一个联合体union Storage {// 方案 A:直接存指针(用于堆分配的大对象)CallableBase* heap_ptr;// 方案 B:预留的一块栈内存(比如 16 或 32 字节)char stack_buffer[32]; } storage;// 一个函数指针,用于管理 storage 中的对象(如何调用,如何销毁,如何复制)// 这其实是另一种形式的手动虚表(V-Table)void (*manager_func)(Storage&, Action, ...); 
};

它是如何工作的?

  1. 当你存入一个函数指针(8字节)时:
    • sizeof(FuncPtr) <= sizeof(stack_buffer)
    • 不需要 new!直接用 placement new 把函数指针拷贝到 stack_buffer 这块内存里。
    • 性能极快,就像拷贝一个 long long
  2. 当你存入一个捕获了 1KB 数据的 Lambda 时:
    • sizeof(Lambda) > sizeof(stack_buffer)
    • 触发堆分配:storage.heap_ptr = new CallableImpl<BigLambda>(...)
    • 性能较慢,涉及 malloc

总结:性能开销到底在哪?

理解了原理,就能精准分析性能损耗:

  1. 虚函数开销(间接寻址):
    • 普通函数调用:Call 0x12345678(直接跳转,CPU 分支预测极准)。
    • std::function 调用:
      1. 读取 storage 中的指针。
      2. 读取对象的虚表指针 vptr。
      3. 查找虚表中的 call 函数地址。
      4. 跳转。
    • 后果: 这会导致指令流水线中断,且难以被编译器内联(Inline)优化。
  2. 缓存命中率 (Cache Locality):
    • 如果触发了堆分配,代码和数据(Lambda 捕获的变量)分散在堆的各处,可能导致 CPU 缓存未命中(Cache Miss)。
    • 如果是 SBO(栈上分配),数据就在当前栈帧,缓存极其友好。
http://www.jsqmd.com/news/115924/

相关文章:

  • function bind
  • 日记12,19
  • Item10--令赋值操作符返回一个
  • Item9--绝不在构造和析构过程中调用虚函数
  • python django flask考研互助交流平台_c62p51fu--论文
  • 日记12.18
  • 离散化遍历
  • Ubuntu上使用VScode创建Maven项目
  • 线程(2)
  • 大规模语言模型的抽象思维与创新能力培养
  • 线程(1)
  • 方达炬〖发明超新技术〗:冰堆技术;冷极冰堆建筑技术;
  • Item6--若不想使用编译器自动生成的函数,就该明确拒绝
  • 我发现LLM解析基因数据优化抗癌药剂量,患者副作用直降40%
  • 日记12.16
  • 论文AIGC查重率高怎么办?6个降AI率工具和技巧,AI率从100%降到3%! - 还在做实验的师兄
  • PCL曲面重建——为一组点云重建凸多边形/凹多边形
  • 信息与关系:涌现的三大核心原则
  • Linux文件权限
  • 28
  • 灵遁者:量子基元理论带来的新观点
  • 【补充】远程连接学校服务器操作说明
  • 本地私有知识库新选择:访答软件真实体验分享
  • 划分dp
  • 花边服饰银发红眸者山间近景
  • 日记12,15
  • Item4--确定对象被使用前已先被初始化
  • string_view
  • 当K3s遇见RustFS:轻量级边缘存储方案的探索与实践
  • 比话降AI靠谱吗?比话能降知网AI率吗? - 还在做实验的师兄