在 C++ 中,std::function 是一个极其强大且灵活的通用多态函数包装器 。它定义在 <functional> 头文件中,能够存储、复制和调用任何可调用目标(Callable Target)。
1. 核心定义与语法
std::function 的模板参数是一个函数签名 ,格式为 返回值类型(参数列表)。
C++
#include <functional>
// 声明一个返回值为 int,接受两个 int 参数的函数包装器
std::function<int(int, int)> func;
2. 它可以包装哪些对象?
std::function 的魅力在于它的"杂食性",它可以统一处理以下所有类型:
-
普通函数
-
Lambda 表达式
-
函数对象(重载了
operator()的类实例) -
类成员函数 (需配合
std::bind或 Lambda) -
类静态成员函数
3. 基本用法示例
包装普通函数与 Lambda
C++
int add(int a, int b) { return a + b; }
std::function<int(int, int)> f;
f = add; // 普通函数
f = [](int a, int b) { // Lambda
return a * b;
};
包装类成员函数
包装成员函数时,必须指明它是哪个对象的成员。通常有两种方式:
-
使用
std::bind -
使用 Lambda(推荐)
C++
struct Foo {
void print_add(int i) const { std::cout << i << '\n'; }
};
Foo foo;
// 方式 1:Lambda
std::function<void(int)> f1 = [&](int i) { foo.print_add(i); };
// 方式 2:std::bind
std::function<void(int)> f2 = std::bind(&Foo::print_add, &foo, std::placeholders::_1);
4. 关键特性
类型擦除 (Type Erasure)
这是 std::function 的底层核心。它通过内部的一套模板机制,把不同类型的可调用对象"包装"起来,对外呈现统一的接口。这让你可以把不同的 Lambda 或函数存入同一个容器:
C++
std::vector<std::function<void()>> tasks;
tasks.push_back([](){ /* 任务A */ });
tasks.push_back(std::bind(some_function));
空检查
std::function 可以处于"空"状态(即不指向任何目标)。调用空的 std::function 会抛出 std::bad_function_call 异常。
C++
std::function<void()> f;
if (!f) { // 或者 if (f == nullptr)
// 尚未赋值,直接调用会崩溃
}
小对象优化 (SBO)
为了性能,大多数 std::function 的实现包含一个小缓冲区。如果存储的可调用对象足够小(如没有捕获变量的 Lambda),它会直接存在栈上;如果对象太大,则会触发动态内存分配(堆分配)。
5. 性能权衡
虽然 std::function 很方便,但它并非没有代价:
| 特性 | 影响 |
|---|---|
| 虚函数调用 | 内部通常涉及间接调用(类似虚函数),无法像直接调用 Lambda 那样被编译器内联。 |
| 内存分配 | 捕获大量变量的 Lambda 可能导致堆内存分配。 |
| 类型安全 | 提供了运行时的灵活性,但在某些性能敏感场景下(如高频循环),直接使用模板函数会更快。 |
6. 与 std::bind 的关系
在 C++11 时代,两者经常配合使用。但随着 C++14/17 对 Lambda 的增强(如泛型 Lambda),std::bind 的使用场景正在萎缩。
建议: 除非需要处理非常复杂的参数重排,否则优先使用 Lambda 表达式来初始化
std::function,这样代码更易读,性能通常也更好。
7. 常见应用场景
-
回调系统:定义 UI 组件的点击事件处理。
-
任务队列:线程池中存储异步执行的任务。
-
策略模式:在运行时动态更换算法逻辑。