你有没有遇到过这种场景——
你写了一个函数,需要接受一个“回调”,但这个回调可能是:一个普通函数、一个 lambda 表达式、一个仿函数(函数对象),甚至是一个 bind 表达式。
你心想:“这可咋整?难道要我写一堆重载?或者模板套模板?要是能用一种统一的方式存储和传递它们就好了。”
正当你抓耳挠腮时,C++11 给你送来了一位“万能劳务中介”——std::function。
它是一个通用的多态函数包装器,能够存储、复制和调用任何可调用对象。
那么今天我们就来扒一扒这个“万能劳务中介”的底裤。
std::function是什么
std::function 是 C++11 引入的一个标准库模板类,它本质上是一个通用的多态函数封装器。
通俗点说:它就是一个可以存储、复制、调用任何可调用对象的“万能盒子”——只要那个可调用对象的签名(即参数类型和返回类型)和这个盒子对得上。
它在头文件:' #include ' 中。
它的声明长这样(简化版):
template<class Signature>
class function;
这里 Signature 是一个函数类型,比如 int(int, double) 表示这个 function 接受两个参数(int 和 double),返回 int。
完整的类模板还有一堆成员函数、类型定义和运算符重载,但最核心的骨架就是:
template<class R, class... Args>
class function<R(Args...)> {
// 构造函数、赋值、swap、operator bool、operator() 等
};
也就是说,当你写下 std::function<int(int, double)> func 时,编译器实际上为你生成一个专门处理签名 int(int, double) 的 function 特化版本。
任何可以调用的、其参数和返回值类型与Signature兼容的实体。
包括普通函数、函数指针、lambda、std::bind生成的函数对象、函数对象类(重载operator()的类的实例)、成员函数指针(需要指定对象)等。
注意,它要求可调用对象的拷贝是可用的,因为std::function通常存储其副本。
基本用法示例
让我们抛开枯燥的理论,直接开始动手!
普通函数
我们先定义一个简单的函数:
#include <functional>
#include <iostream>
int cook(int a, int b) {
std::cout << a << " + " << b << " = " << a+b << std::endl;
return a + b;
}
然后直接把函数名赋值给 std::function,简单粗暴。
int main()
{
std::function<int(int, int)> func = cook; // 函数名自动退化成函数指针
func(3, 5); // 输出:3 + 5 = 8
return 0;
}
无需加工,简单直接。
函数指针
与普通函数一样:
int (*funcPtr)(int,int) = cook; // 定义函数指针
std::function<int(int,int)> func2 = funcPtr;
func2(7, 2); // 输出:7 + 2 = 9
lambda表达式
Lambda 是 C++ 里最灵活的可调用对象,std::function 自然来者不拒。
auto lambda = [](int a, int b) {
std::cout << a << " * " << b << " = " << a * b << std::endl;
return a * b;
};
std::function<int(int, int)> func3 = lambda;
func3(4, 6); // 输出:4 * 6 = 24
在上述代码中,lambda表达式创建了一个闭包对象,
然后被存储在std::function(int<int,int>)类型的对象func3中。
当func3被调用时,std::function内部机制会解包lambda表达式的闭包状态。
仿函数
只要一个类重载了 operator(),它的实例就是一个函数对象。
struct Multiplier {
int operator()(int a, int b) const {
std::cout << a << " * " << b << " = " << a * b << std::endl;
return a * b;
}
};
Multiplier mul; // 创建函数对象
std::function<int(int, int)> func4 = mul;
func4(3, 7); // 输出:3 * 7 = 21
std::bind 产生的绑定表达式
std::bind 可以把一部分参数固定下来,生成一个可调用对象。
int add(int a, int b, int c) { // 三个参数的函数
std::cout << a << " + " << b << " + " << c << " = " << a + b + c << std::endl;
return a + b + c;
}
auto bound = std::bind(add, 10, std::placeholders::_1, 20); // _1用来占位,是将来传入的第一个参数
std::function<int(int)> func5 = bound; // 注意签名变成了 int(int)
func5(5); // 输出:10 + 5 + 20 = 35
std::bind 使用占位符来表示未绑定的参数,这些占位符决定了在生成的新函数对象中如何传递参数。
常见的占位符有:
- std::placeholders::_1
- std::placeholders::_2
- std::placeholders::_3
- 等等
成员函数指针
成员函数指针不能直接存进 std::function,因为它需要绑到一个对象上才能调用。
常见的包装方式有两种:std::bind 或 lambda。
方法1:使用 std::bind:
struct Chef {
void cookDinner(int count) const {
std::cout << "count value: " << count << std::endl;
}
};
Chef myChef;
auto boundMember = std::bind(&Chef::cookDinner, &myChef, std::placeholders::_1);
std::function<void(int)> func6 = boundMember;
func6(3); // 输出:count value: 3
方法2:使用 lambda:
std::function<void(int)> func7 = [&myChef](int count) {
myChef.cookDinner(count);
};
func7(5); // 输出:count value : 5
成员函数就像一个需要主人才能启动的机器人,你得用 bind 或 lambda 给它配上主人,然后 std::function 才能愉快地指挥它。
另一个 std::function 对象
std::function 本身也是可拷贝的,所以你可以把一个 std::function 赋值给另一个(只要签名相同)。
std::function<int(int, int)> original = cook;
std::function<int(int, int)> copy = original; // 拷贝构造
copy(2, 4); // 输出:2 + 4 = 6
这就像俄罗斯套娃一样。
std::function 凭借类型擦除,让 C++ 的函数式编程变得异常灵活。
你可以把各种不同“形状”的可调用对象统一成同一种类型,放进容器、作为回调参数、或者实现策略模式。
它唯一的“代价”就是运行时可能有一次间接调用(类似虚函数),但这点开销在大多数场景下都可以忽略不计。
使用技巧与注意事项
现在我们要讨论一些陷阱和技巧。
空状态检查
std::function 默认构造出来是空的,里面啥也没有。
如果你胆敢直接调用它:
std::function<int(int, int)> empty;
int result = empty(1, 2); // 抛出 std::bad_function_call 异常
程序会当场给你好看。所以,调用之前一定要检查它是不是空的:
if (empty) // operator bool 检查是否存储了可调用对象
{
empty(1, 2);
}
else
{
std::cout << "std::function is nullptr." << std::endl;
}
你也可以主动赋值为nullptr清空它:
std::function<int(int,int)> func = someCallable;
func = nullptr; // 现在 func 又空了
性能开销
我们先来看一段代码:
auto lambda1 = []() {};
std::function<void()> func = lambda1;
std::cout << "lambda1 sizeof: " << sizeof(lambda1) << std::endl;
std::cout << "func sizeof: " << sizeof(func) << std::endl;
程序输出:
lambda1 sizeof: 1
func sizeof: 64
(上述输出的std::function大小,因编译器和平台而不同)
为什么空 lambda 的大小是 1:
- 因为lambda {} 没有任何捕获,是一个空类类型(closure type)。
- C++ 规定,任何对象都必须拥有唯一的地址。为了让同一个空类的不同对象能够有不同的地址,编译器会给空类隐含地添加一个无用的字节(或类似机制),因此 sizeof 通常返回 1。
std::function 的大小为什么会这么大?
std::function 是一个类型擦除的包装器,它可以存储任何可调用对象(函数指针、lambda、函数对象等)。
为了实现这种通用性,它内部需要:
- 存储实际的可调用对象(或指向它的指针)
- 存储一组“管理函数”的指针,用于复制、销毁、调用等操作(类似于虚表)
- 支持小对象优化:对于小型可调用对象(如空 lambda),直接存储在对象内部,避免堆分配;对于大型对象,则在堆上分配,并在 std::function 内部只保存一个指针。
因此,std::function 对象内部通常会包含一个联合体(union),既可以容纳足够大的静态缓冲区,又可以容纳指向堆上控制块的指针。
此外,还需要几个指针来指向管理函数。
为了直观,我们假设一个简化版的 std::function 布局,它包含:
- 一个函数指针(指向调用前的处理)
- 一个指向“管理表”的指针(包含复制、销毁等操作)
- 一个联合体(_Storage),用于存放实际可调用对象(小对象优化)或指向堆上控制块的指针。
堆上的控制块(当不使用小对象优化时)可能长这样:
当使用小对象优化时,lambda 直接存放在 _Storage 中,不需要额外堆分配。
由于 lambda 是空对象,它正好被放进静态缓冲区中,所以没有堆分配开销。
但 std::function 对象本身依然保留了完整的存储空间(64 字节),以便能处理更大的可调用对象。
总结:std::function 是类型擦除容器,内部包含一个静态缓冲区(用于小对象优化)和若干管理指针,因此体积较大。
返回引用的陷阱
std::function 可以返回引用,比如 std::function<int&(int)>。这就有意思了——如果它内部存储的可调用对象返回了一个局部变量的引用,那你就得到了一个悬垂引用,程序可能随时崩溃。
看一个作死的例子:
std::function<const std::string& ()> badFunc = []() -> const std::string& {
std::string local = "temporary";
return local; // 返回局部对象的引用
}; // lambda 结束,local 销毁
std::string s = badFunc();
std::cout << s << std::endl; // 未定义行为,访问已销毁的对象
同样,如果 lambda 通过引用捕获了一个局部变量并返回它,也可能出问题。
因此我们应该按值返回,而不是返回引用。
或者确保被引用的对象生命周期长于 std::function。
使用 std::move 优化
那么我们来介绍一下该如何使用 std::move 进行优化。
std::function 内部可能持有一些资源,比如:
- 小对象(比如几个 int 捕获的 lambda)——可能存储在对象内部的小对象缓冲区,移动时只需要复制几个字节。
- 大对象(比如捕获了复杂数据结构的 lambda)——可能通过堆分配存储,内部是一个指向堆内存的指针。
移动语义可以让 std::function 在赋值或构造时转移所有权,而不是深拷贝内部资源。尤其当内部有堆分配时,移动避免了昂贵的堆内存复制,只需交换指针。
移动 vs 拷贝:性能对比
拷贝 std::function 就像把一个装满货物的集装箱完整复制一遍——耗时又占空间;而移动则像直接转移集装箱的所有权,只需交接一张钥匙卡,瞬间完成。
我们先来看个例子:
std::function<int(int)> createBigFun(bool useMove)
{
// 假设这个 lambda 捕获了一个巨大的 vector,导致堆分配
std::vector<int> huge(1000000);
// 在捕获前显式制造一次拷贝
if (useMove) {
return [huge = std::move(huge)](int x) { return x + huge[0]; }; // 捕获初始化,移动捕获
}
else {
return [huge](int x) { return x + huge[0]; }; // 按值捕获,拷贝 huge
}
}
int main() {
auto start = std::chrono::steady_clock::now();
std::function<int(int)> f1 = createBigFun(false); // 进行拷贝
auto end = std::chrono::steady_clock::now();
std::cout << "f1耗时: "
<< std::chrono::duration<double, std::milli>(end - start).count()
<< " ms" << std::endl;
start = std::chrono::steady_clock::now();
std::function<int(int)> f2 = createBigFun(true); // 进行移动
end = std::chrono::steady_clock::now();
std::cout << "f2耗时: "
<< std::chrono::duration<double, std::milli>(end - start).count()
<< " ms" << std::endl;
return 0;
}
程序输出:
f1耗时: 1.6646 ms
f2耗时: 0.7128 ms
我们可以看到在捕获前进行拷贝和在捕获前进行移动,它们在性能上还是差别蛮大的。
我们再来看一个例子,看看拷贝构造和移动构造的性能差别:
int main() {
auto start = std::chrono::steady_clock::now();
std::function<int(int)> f3 = f1; // 拷贝构造 → 深拷贝内部资源
auto end = std::chrono::steady_clock::now();
std::cout << "拷贝耗时: "
<< std::chrono::duration<double, std::milli>(end - start).count()
<< " ms" << std::endl;
start = std::chrono::steady_clock::now();
std::function<int(int)> f4 = std::move(f1); // 移动构造 → 仅转移资源,f1 变为空
end = std::chrono::steady_clock::now();
std::cout << "移动耗时: "
<< std::chrono::duration<double, std::milli>(end - start).count()
<< " ms" << std::endl;
return 0;
}
程序输出:
拷贝耗时: 0.9145 ms
移动耗时: 0.0017 ms
现在更能直观的看到它们之间的区别了。
- 拷贝:可能触发堆内存的完整复制,开销与对象大小成正比。
- 移动:通常只是交换内部指针,常数时间,而且源对象变为空。
小贴士:std::function 对小尺寸的可调用对象(如无捕获的 lambda)可能使用内部缓冲区,此时拷贝和移动的代价都很小。我的例子中故意用大捕获对象,就是为了绕过小对象优化,让深拷贝的代价暴露出来。
哪些场景适合用 move 优化?
1. 将 std::function 存入容器时
如果我们有一个 vector<std::function<void()>>,并且我们不再需要原始的 function 对象,用 emplace_back(std::move(func)) 或者 push_back(std::move(func)) 可以避免拷贝。
std::vector<std::function<void()>> tasks;
std::function<void()> task = []{ /* 复杂操作 */ };
tasks.push_back(std::move(task)); // 移动,不再拷贝
2. 作为函数参数传递时(尤其是回调)
如果函数接受 std::function 作为参数,并且我们希望将所有权转移进去,用移动可以省去一次拷贝。
void enqueue(std::function<void()> task)
{
// 假设这里将 task 存入队列
queue_.push_back(std::move(task));
}
// 调用方:
std::function<void()> myTask = []{ doWork(); };
enqueue(std::move(myTask)); // 移动传递
3. 返回 std::function 时(已经自动优化)
现代 C++ 编译器通常会应用 RVO(返回值优化) 或隐式移动。当我们返回一个局部 std::function 时,一般不需要显式 std::move——显式写反而可能阻止某些优化。让编译器自己决定最优雅。
移动能消除“性能开销”吗?
要分清楚:移动优化的是对象的构造/赋值开销,而不是调用开销。
- 调用 std::function 时的间接调用(类似虚函数)依然存在,这是类型擦除的固有成本,移动无法消除。
- 但如果我们的瓶颈是频繁拷贝 std::function(比如在容器中多次复制),移动能大幅降低这部分开销。
应用场景
回调函数
假设你需要调用某个库函数,并希望它在某个时刻“回拨”你的代码——比如下载完成时通知你、定时器到期时执行你的逻辑。这就是回调函数。
因为std::function的回调函数的签名是固定的(比如 void(int)),但具体要执行的动作可以是普通函数、lambda 或者成员函数。
std::function 可以统一这些类型,让你轻松传递。
代码示例:
void download(std::function<void(int)> onComplete)
{
// 模拟下载
int result = 42;
onComplete(result); // 下载完成,回调
}
void myCallback(int x) { std::cout << "下载完成,结果:" << x << std::endl; }
int main()
{
download(myCallback); // 普通函数
download([](int x) { std::cout << "lambda: " << x << std::endl; }); // lambda
return 0;
}
程序输出:
下载完成,结果:42
lambda: 42
策略模式的实现
策略模式定义了一系列算法,将它们一个个封装起来,并使它们可以相互替换。
策略模式让算法的变化独立于使用算法的客户。通常的结构包括:
- 策略接口:定义所有支持的算法的公共接口。
- 具体策略:实现策略接口的具体算法。
- 上下文:持有对策略对象的引用,负责调用策略。
通俗点来说就是你有一个排序算法,但允许用户自定义比较规则;或者一个压缩工具,支持多种压缩算法。这就是策略模式。
std::function 让策略的传递变得简单,不需要定义一堆策略接口类。
下面看一段代码:
class DataProcessor
{
private:
std::function<int(int, int)> strategy;
public:
void setStrategy(std::function<int(int, int)> s) { strategy = s; }
int compute(int a, int b) { return strategy(a, b); }
};
- std::function<int(int, int)> 是一个通用的函数包装器,可以存储任何可调用对象,只要它接受两个 int 并返回 int。这里它充当了策略接口的角色。
- setStrategy:允许客户端在运行时动态设置具体的策略。参数 s 可以是任何符合签名的可调用对象。
- compute:执行当前策略,将两个整数传递给策略对象并返回结果。
使用示例:
DataProcessor dp;
dp.setStrategy([](int a, int b) { return a + b; });
std::cout << dp.compute(3, 4) << std::endl; // 输出:7
dp.setStrategy(std::multiplies<int>()); // 标准库函数对象
std::cout << dp.compute(3, 4) << std::endl; // 输出:12
- 第一次 setStrategy 传入一个 lambda 表达式,实现了加法运算。
- 第二次 setStrategy 传入标准库函数对象 std::multiplies,它重载了operator() 执行乘法。
第二次调用会覆盖第一次设置的策略,因此dp 最后一次设置的策略将是乘法。
策略模式的好处:
- 开闭原则:新增策略无需修改 DataProcessor 类,只需定义新的可调用对象并传递给 setStrategy。
- 运行时灵活性:可以在程序运行中根据条件改变算法。
- 解耦:DataProcessor 不依赖具体的算法实现,只依赖抽象的 std::function 签名。
函数表(命令分发)
假设你需要根据用户输入的命令字符串,执行对应的操作,比如命令行工具中的 help, quit等。
那么你可以建立一个从字符串到函数的映射(函数表),根据输入快速找到并调用对应的函数。
std::function 能统一所有命令函数的签名。
std::map<std::string, std::function<void()>> commands;
commands["help"] = [] { std::cout << "显示帮助信息" << std::endl; };
commands["quit"] = [] { exit(0); };
std::string cmd;
std::cout << "Enter help or quit: " << std::endl;
while (std::cin >> cmd)
{
if (commands.count(cmd)) commands[cmd]();
else std::cout << "未知命令" << std::endl;
}
程序输出:
Enter help or quit:
help
显示帮助信息
add
未知命令
quit
线程池
假设线程池需要接收用户提交的任务(可以是任何可调用对象),并在某个线程中执行。
使用 std::function:任务五花八门,但线程池只需要知道任务无参数无返回值(或统一签名)即可。
std::function 可以擦除具体类型,让线程池只关心调用。
class ThreadPool {
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
public:
void enqueue(std::function<void()> task)
{
tasks.push(task);
// 通知一个工作线程取任务...
}
// ... 线程执行循环
};
ThreadPool pool;
pool.enqueue([] { std::cout << "任务1" << std::endl; });
pool.enqueue(std::bind(someFunction, 42));
总结
std::function在性能敏感的代码中,记得考虑它带来的间接开销;在只需要一种类型的场景,auto 可能更合适。
但只要需要类型擦除、统一容器或接口,std::function 绝对是不二之选。