c++23的std::function_ref与普通函数引用有什么区别? (零开销回调)

std::function_ref 是零开销、非拥有、类型擦除的只读回调视图,内部仅存 void* 和函数指针,不分配内存、不复制可调用体,但不管理生命周期,误用将导致悬垂引用或未定义行为。

std::function_ref 是类型擦除的只读视图,不是函数引用

很多人看到 std::function_ref 就以为它是类似 int& 那样的原生引用,其实完全不是。它不持有任何对象,也不参与所有权管理,只是一个轻量级的、非拥有的、类型擦除的回调视图。它的核心目标是:在不分配内存、不复制可调用体的前提下,把任意可调用物(lambda、函数指针、std::function、绑定对象等)统一接入同一接口。

为什么不能直接用 T& 或 const T& 代替?

普通引用要求编译期知道具体类型,而回调场景往往需要“接受多种可调用类型”的统一参数签名。比如一个日志函数想同时接受 void()void(int)const char*() 等不同签名的可调用体——这无法用单个模板参数 T& 实现,因为每个 T 都是不同类型,函数重载或模板推导会爆炸。

  • std::function_ref绑定 []{}&my_free_funcstd::function、甚至捕获了局部变量的 lambda(只要不逃逸)
  • auto&& fconst auto& f 虽然也能转发,但无法作为函数参数统一声明;写成模板又导致实例化膨胀
  • std::function 会触发堆分配(除非小对象优化生效),且拷贝有开销;std::function_ref 完全避免这两点

std::function_ref 的零开销怎么来的?

它内部只存两个字段:一个指向可调用体的 void*(或类似指针),一个指向调用分发函数的函数指针。两者加起来通常就是 16 字节(x64),且所有操作都是纯指针解引用 + 间接跳转,无虚函数表、无 new/delete、无异常传播开销。

void example(std::function_ref cb) {
    int result = cb(3.14); // 直接调用,无额外分支或检查
}
  • 传入的 lambda 若无捕获,cb 内部存储的是其地址 + 编译器生成的静态调用桩
  • 传入带捕获的 lambda,cb 存储的是捕获块地址 + 对应的调用桩(该桩由标准库为每种签名生成一次)
  • 传入 std::functioncb 仅借用其存储区和调用逻辑,不复制内容
  • 注意:std::function_ref 不延长所引用对象的生命期——若传入栈上 lambda 并保存其 function_ref 到作用域外,就是悬垂引用

常见误用:把它当 std::function 用或跨作用域保存

最典型的坑是把它当成“轻量版 std::function”来长期持有回调。它没有所有权语义,也没有移动/拷贝构造的安全保障(拷贝是浅复制,移动未定义)。

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

  • 不要把 std::function_ref 成员变量存进类里,除非你 100% 控制被引用对象的生命周期长于该类
  • 不要从函数返回 std::function_ref,除非返回的是全局函数或静态 lambda
  • 不能对临时 lambda 写 std::function_ref{[]{}}() —— 临时对象在表达式结束就销毁,引用立刻悬垂
  • 它不支持 nullptr 检查(没有空状态),也不能赋值(只有构造)
<:function_ref> 的价值不在语法糖,而在明确表达“我只读、不拥有、不分配、请保证生命周期”的契约。一旦忽略这个前提,零开销就立刻变成未定义行为。