CPP学习笔记—std::function的用法
std::function 是 C++11 中引入的一个极其有用的工具,位于 <functional> 头文件中。它是一个通用的、多态的函数包装器。它的实例可以存储、复制和调用任何可调用 (Callable) 目标——包括普通函数、Lambda 表达式、函数指针、成员函数指针、以及函数对象(functors)。
1. std::function 是什么?
一句话定义
std::function 是一个类型安全的包装器,它可以持有任何符合其函数签名的可调用对象。
可以把它想象成一个“万能的函数指针”,但它比函数指针强大得多,因为它可以指向任何可调用的东西,而不仅仅是全局函数。
解决的问题:类型不统一的“可调用物”
在 C++ 中,有很多东西都可以被“调用”,比如:
- 普通函数指针:
void (*p_func)(int); - 函数对象 (Functor): 一个重载了
operator()的类的对象。每个函数对象都有自己独特的类型。 - Lambda 表达式: 编译器会为每个 Lambda 生成一个唯一的、匿名的闭包类型。
在 std::function 出现之前,如果你想编写一个函数来接受一个“回调”,你将面临一个难题。例如,一个按钮的点击事件处理:
1 | // 我想让这个函数能接受任何“无参数、无返回值”的回调 |
这里的 ??? 应该是什么类型?
- 如果是
void (*)(),那么它就不能接受带捕获的 Lambda 或函数对象。 - 如果使用模板
template <typename T> void set_on_click_handler(T handler),那么这个类型会“渗透”到你的类定义中,使其也必须是模板,这会大大增加复杂性。
std::function 完美地解决了这个问题。它提供了一个统一的类型来表示所有具有相同签名的可调用对象。
1 |
|
现在,任何无参数、无返回值的可调用物都可以传递给这个函数了。
2. 基本语法和用法
包含头文件
1 |
声明与初始化
std::function 是一个类模板,其模板参数是函数签名。
1 | // 声明一个可以持有“接受一个 int 和一个 double,返回一个 bool”的函数的包装器 |
调用
调用 std::function 对象就像调用一个普通函数一样。
1 | double result = func_divider(10, 3); // result is 3.333... |
检查是否为空
一个未初始化的 std::function 对象是“空的”。在调用之前检查它是否持有可调用对象是一个好习惯,否则会抛出 std::bad_function_call 异常。
1 | std::function<void()> empty_func; |
3. std::function 可以存储什么?
这是 std::function 最强大的地方。只要签名匹配,它可以存储几乎任何可调用物。
示例1:普通函数 (Free Functions)
1 |
|
示例2:Lambda 表达式
这是最常见的用法,尤其是对于带状态(有捕获)的 Lambda。
1 | int main() { |
示例3:函数对象 (Functors)
一个重载了 operator() 的类的实例。
1 | struct Multiplier { |
示例4:类的成员函数
这是稍微复杂一点的情况,因为成员函数需要一个 this 指针(即一个类的实例)来调用。通常有两种方法来包装它们。
方法 A:使用 std::bind (传统方式)
std::bind 可以将成员函数、对象实例和参数“绑定”在一起,生成一个新的可调用对象。
1 |
|
方法 B:使用 Lambda 表达式 (现代 C++ 推荐方式)
Lambda 表达式通常更清晰、更易读。
1 | int main() { |
4. std::function 的内部机制与关键特性
类型擦除 (Type Erasure)
std::function 的核心魔法是类型擦除。当你把一个 Lambda (类型为 ClosureType_A)或者一个 Functor (类型为 MyFunctor)赋值给 std::function<void()> 时,std::function 内部会通过动态内存(通常)和虚函数来“擦除”原始对象的具体类型,只保留其“可调用性”和函数签名。
它内部大致像这样(极度简化):
1 | // 伪代码 |
这就是为什么它可以接受不同类型的可调用对象——它们都被包装在了一个统一的接口后面。
小对象优化 (Small Object Optimization, SOO)
由于类型擦除通常需要堆分配(new),这会带来性能开销。为了缓解这个问题,大多数标准库的 std::function 实现都采用了小对象优化。
如果赋给 std::function 的可调用对象足够小(比如一个不捕获任何东西的 Lambda 或一个小的函数对象),它会被直接存储在 std::function 对象自身的内存缓冲区中,从而避免了堆分配。这大大提高了在这些常见情况下的性能。
性能考量
- 调用开销: 调用
std::function通常比直接调用函数或通过函数指针调用要慢,因为它可能涉及一次虚函数分派。 - 构造/赋值开销: 如果没有触发小对象优化,构造或赋值
std::function会有一次堆内存分配的开销。 - 内存占用:
std::function对象本身的大小通常是几个指针的大小,用于存储内部缓冲区和管理信息。
5. 实际应用场景
场景1:实现回调函数系统
这是 std::function 的经典用途。比如一个定时器类,它在特定时间后执行一个回调。
1 | class Timer { |
场景2:作为策略模式的轻量级替代方案
在策略模式中,我们通常定义一个接口和多个实现类。如果策略很简单,std::function 可以提供一种更轻量级的方法。
1 | // Context |
场景3:在容器中存储不同的操作
比如,构建一个命令分派器。
1 | std::map<std::string, std::function<void()>> command_map; |
6. 注意事项与最佳实践
空 std::function 的调用
如前所述,调用一个空的 std::function 会抛出 std::bad_function_call 异常。在调用前务必检查。
捕获的生命周期问题
当 Lambda 捕获变量时(尤其是按引用捕获 [&]),你需要非常小心。
1 | std::function<void()> create_bad_callback() { |
规则: 确保被 Lambda 捕获的对象的生命周期不短于 std::function 对象本身及其任何副本的生命周期。对于局部变量,要么按值捕获 [=],要么使用 std::shared_ptr 等智能指针来管理生命周期。
与模板的对比:何时使用 std::function
使用模板: 当你可以在编译时确定可调用对象的类型,并且不需要在运行时更改它时,优先使用模板。模板会为每种类型生成专用代码,编译器可以进行内联等优化,性能最高(通常是零开销)。
1
2
3
4template <typename Callable>
void run_task(Callable task) {
task(); // 编译器可以直接内联
}使用
std::function: 当你需要在运行时存储或传递一个类型未知的可调用对象时,使用std::function。这是典型的“运行时多态”场景,比如存储回调的容器、需要动态切换的策略等。你为此付出的代价是潜在的性能开销。
7. 总结
std::function 是现代 C++ 中一个强大而灵活的组件。它通过类型擦除技术,为 C++ 中各种形式的“可调用物”提供了一个统一的、类型安全的接口。
要点回顾:
- 它是通用的函数包装器。
- 它能存储函数、Lambda、Functor、成员函数等。
- 它是实现回调、事件处理和策略模式的利器。
- 使用它需要注意生命周期和性能开销。
- 在性能敏感且类型可在编译期确定的场景下,模板是更好的选择。










