资讯动态

C++ std::function详解

发布时间:2026/10/21 4:49:09 来源:尧图企业网站定制
前言std::function是 C11 引入的通用多态函数包装器定义在functional中。它可以把函数指针、lambda、std::bind的结果、重载了operator()的类对象统一存成同一种类型例如std::functionint(int)。它的价值在于类型擦除调用方不需要知道具体可调用对象是什么类型只需要知道签名。围绕它有四个常见误解。第一「std::function是零开销抽象」——它内部通常有一次间接调用还可能需要堆分配是否可接受取决于调用频率。第二「空的std::function调用什么也不做」——调用空对象会抛出std::bad_function_call。第三「什么可调用对象都能装」——C17 的std::function要求可调用对象可拷贝构造捕获了std::unique_ptr的 lambda 装不进去。第四「std::function就是函数指针」——函数指针只是它支持的一种输入二者的存储模型和开销完全不同。本文讲清类型擦除的实现思路、构造与调用方式、成员函数指针的适配、性能边界以及 C23 的std::move_only_function作为补充。所有示例以 C17 为基准。一、类型擦除与对象模型std::functionR(Args...)的内部需要做两件事保存一个任意类型的可调用对象在调用时以R(Args...)的签名调用它。这两件事无法用普通成员变量完成因为类型在编译期未知所以标准库采用类型擦除内部持有一个基类指针或等价机制派生类模板保存具体的可调用对象并通过虚函数完成调用与拷贝。由此可以推出几个行为特征。第一std::function的大小是固定的但标准没有规定具体值也没有规定是否使用小对象优化small buffer optimization, SBO。常见实现会在对象较小且可平凡搬移时直接存放在内部缓冲区避免堆分配但这只是实现策略不是可移植保证。第二拷贝一个std::function会拷贝其内部的可调用对象如果该对象持有资源拷贝可能分配内存。第三std::function的调用通常是间接调用编译器难以像对待普通函数那样内联。标准对模板参数的要求是被存储的可调用对象必须满足CopyConstructible并且能用给定的实参类型调用、返回值能转换为R。这个「可拷贝」要求是 C17 下最大的限制后文会讲替代方案。二、构造、调用与成员函数最基本的用法是把 lambda 赋给std::function// 适用于 C17#include functional#include iostreamint main() {std::functionint(int) twice [](int x) { return x * 2; };std::cout twice(21) \n;std::functionint(int) empty; // 空状态std::cout static_castbool(empty) \n; // 0if (empty) {empty(1); // 不会执行}}空std::function的operator bool返回 false对它调用operator()会抛出std::bad_function_call定义在functional该异常派生自std::exception。因此「可能为空」的回调在调用前必须检查或者用try/catch兜底。不要假设空调用是无操作。函数指针可以直接赋值但重载函数需要先消除歧义// 适用于 C17#include functionalint process(int x) { return x 1; }double process(double x) { return x 0.5; }void demo() {std::functionint(int) f static_castint(*)(int)(process);std::functiondouble(double) g static_castdouble(*)(double)(process);}成员函数指针也能放入std::function但签名要把对象作为第一个参数// 适用于 C17#include functional#include iostream#include stringstruct Greeter {std::string prefix;void greet(const std::string name) const {std::cout prefix name \n;}};int main() {Greeter g{Hello, };std::functionvoid(const Greeter, const std::string) direct Greeter::greet;direct(g, Ada);std::functionvoid(const std::string) bound [g](const std::string n) {g.greet(n);};bound(Bob);}direct保存的是成员函数指针调用时通过第一个实参提供对象bound用 lambda 把对象固定下来。两者语义不同前者每次调用可以传不同对象后者绑定到g。若用std::bind(Greeter::greet, g, std::placeholders::_1)也能达到绑定效果但 lambda 更直观也更容易看出捕获的是指针还是引用。用std::bind时如果传的是对象指针g销毁后调用就是悬垂访问传对象副本则会拷贝语义又不同。std::function还提供target_type()与targetT()用于查询内部实际存储的类型。但 lambda 的类型是编译器生成的未命名闭包类型无法在源码里写出T因此对 lambda 调用targetT()实际上拿不到这两个接口主要用于「存的是不是某个函数指针或已知函数对象」的判断调试时target_type().name()的输出也是实现定义的。三、性能边界与替代方案std::function的成本来自三处间接调用难以内联、可能的堆分配可调用对象超过内部缓冲区时、拷贝时的深拷贝。标准不保证 SBO因此不能假设小 lambda 一定不分配。以下场景应当谨慎场景建议高频调用的热路径用模板参数或函数指针让调用点可见只是把回调传进函数一次用模板template class F void run(F f)避免类型擦除需要保存异构回调集合std::function合理回调可能为空调用前检查或改用std::optional 明确的默认行为可调用对象只能移动C17 下不能用std::function最后一条值得展开。C17 的std::function要求可拷贝因此下面的代码无法通过编译// 适用于 C17这段代码无法编译#include functional#include memorystd::functionvoid() f [p std::make_uniqueint(1)] { (void)*p; };lambda 捕获了unique_ptr闭包类型不可拷贝不满足std::function的要求。C23 引入了std::move_only_function允许存储仅可移动的可调用对象签名写法类似std::move_only_functionvoid()使用它需要 C23 及支持该特性的标准库。若受限于 C17可以把状态放到std::shared_ptr里再捕获用共享所有权换取可拷贝性但要清楚这改变了语义。另一个替代是std::function_ref一类的「非拥有引用」视图但截至 C23 它尚未进入标准C26 才计划加入。在此之前需要「借用可调用对象、不取得所有权」时可以自己写一个只保存void*与函数指针的轻量包装或直接用模板参数。不要凭印象使用不存在的标准类型。四、完整示例回调注册与递归下面是一个事件分发的骨架演示std::function的典型用法保存多个回调、在触发时逐个调用、支持移除。为了简洁这里用整数 id 标识回调// 适用于 C17#include functional#include iostream#include string#include unordered_map#include vectorclass EventBus {public:using Handler std::functionvoid(const std::string);int subscribe(Handler handler) {int id nextId_;handlers_.emplace(id, std::move(handler));return id;}void unsubscribe(int id) {handlers_.erase(id);}void publish(const std::string message) const {for (const auto entry : handlers_) {if (entry.second) { // 防御空回调entry.second(message);}}}private:int nextId_ 1;std::unordered_mapint, Handler handlers_;};int main() {EventBus bus;int count 0;int id bus.subscribe([count](const std::string msg) {count;std::cout got: msg \n;});bus.publish(first);bus.unsubscribe(id);bus.publish(second);std::cout count \n;}几个细节subscribe按值接收Handler再std::move进容器避免一次多余拷贝publish是const成员因此遍历时不能修改容器这符合「发布过程中不增删订阅者」的约定如果需要在回调里取消订阅就必须先把回调拷贝出来再调用或改用「标记删除、稍后清理」的策略否则在遍历中擦除元素会使迭代器失效。递归 lambda 是另一个常见需求。lambda 无法直接引用自身通常先把std::function声明出来再按引用捕获// 适用于 C17#include functional#include iostreamint main() {std::functionint(int) fib;fib [fib](int n) - int {return n 2 ? n : fib(n - 1) fib(n - 2);};std::cout fib(10) \n;}这里必须按引用捕获fib按值捕获会在 lambda 构造时拷贝一个尚未赋值的空std::function调用时抛出std::bad_function_call。同时要保证fib的生命周期长于所有调用如果把 lambda 存起来延后执行而fib是局部变量就会悬垂。常见坑点❌ 直接调用可能为空的std::function。✅ 调用前if (f)判断或捕获std::bad_function_call并定义降级行为。❌ 用 lambda 按引用捕获局部变量把std::function存起来异步执行。✅ 按值捕获需要的数据或捕获std::shared_ptr延长生命周期。❌ 认为std::function没有额外开销在热路径里反复构造。✅ 热路径用模板参数或函数指针std::function放在初始化阶段构造一次。❌ 把捕获了std::unique_ptr的 lambda 赋给std::function。✅ C17 下改用std::shared_ptr捕获或升级到 C23 用std::move_only_function。❌ 递归 lambda 按值捕获自身得到空的std::function。✅ 先声明std::function再按引用捕获。❌ 用std::bind绑定对象指针对象销毁后仍调用。✅ 用 lambda 明确捕获方式异步场景用weak_ptr并检查是否失效。❌ 把重载函数名直接赋给std::function。✅ 用static_cast指定函数指针类型或包一层 lambda。❌ 依赖target_type().name()的输出做逻辑判断。✅ 名称是实现定义的需要类型判断时用targetT()配合已知类型。总结主题结论本质类型擦除的可调用对象包装器签名固定空状态operator bool为 false调用抛std::bad_function_call存储要求C17 要求可拷贝仅可移动对象需 C23std::move_only_function开销间接调用可能堆分配标准不保证小对象优化成员函数签名首参为对象或改用 lambda 绑定适用场景回调注册、异构回调集合热路径优先模板std::function是一个「用一点运行时开销换取接口统一」的工具。理解它的类型擦除模型就能预判它的开销来源和拷贝语义记住空调用会抛异常、可调用对象必须可拷贝这两条硬约束就能避开大多数运行期和编译期错误。真正的高频路径应该让类型在编译期可见而不是依赖类型擦除。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑