6. Lambda的存储与传递:std::function、auto存储、作为参数传递
Lambda表达式写出来容易,但怎么存、怎么传,才是真正考验功底的地方。我见过不少同事,Lambda写得飞起,结果一涉及到存储和传递就翻车。说白了,Lambda是个临时对象,你得知道怎么把它「抓住」、怎么把它「递出去」。
6.1 用auto捕获Lambda——最直观的方式
C++11开始,auto可以推导Lambda的类型。每个Lambda都有独一无二的类型,编译器会为它生成一个匿名函数对象。用auto来存,最省事。
auto add = [](int a, int b) { return a + b; };
std::cout << add(3, 4) << std::endl; // 输出7
嗯,这里要注意:auto存储的Lambda,类型是唯一的、不可写的。你不能把另一个Lambda赋给它,哪怕签名一模一样。
auto f = [](int x) { return x * 2; };
// auto f2 = [](int x) { return x * 3; };
// f = f2; // 编译错误!类型不匹配
我在项目中遇到过这种场景:写一个排序比较器,用auto存Lambda,然后传给std::sort。当时觉得挺方便,后来需求变了,需要动态切换比较逻辑……auto这条路就走不通了。
6.2 std::function——万能容器
std::function是一个多态函数包装器。它可以存储任何可调用对象:Lambda、函数指针、函数对象……只要签名匹配就行。
#include <functional>
std::function<int(int, int)> func;
func = [](int a, int b) { return a + b; };
std::cout << func(10, 20) << std::endl; // 30
func = [](int a, int b) { return a * b; };
std::cout << func(10, 20) << std::endl; // 200
你看,同一个func可以换不同的Lambda。这就是std::function的价值——类型擦除。它把具体类型藏起来了,只暴露统一的调用接口。
- auto:零开销,类型唯一,不能赋值给不同Lambda
- std::function:有少量开销(虚函数调用、可能堆分配),支持赋值和重新绑定
我曾经在一个回调系统里用过std::function。一开始觉得性能没问题,后来压测发现高频调用时开销不小。优化方案是:能用模板参数就别用std::function,只有在需要类型擦除时才用它。
6.3 作为参数传递——三种常见方式
把Lambda传给函数,有三种主流写法。我按推荐程度排个序:
| 方式 | 代码示例 | 适用场景 | 开销 |
|---|---|---|---|
| 模板参数 | template<typename F> void call(F f) |
通用、高性能 | 零开销 |
| std::function | void call(std::function<void()> f) |
类型擦除、存储回调 | 中等 |
| auto参数(C++20) | void call(auto f) |
简洁、类似模板 | 零开销 |
模板参数是最灵活的。你想想看,编译器会为每个Lambda生成特化版本,没有虚函数开销,还能内联。
template <typename Func>
void repeat(int n, Func f) {
for (int i = 0; i < n; ++i) {
f(i);
}
}
// 调用
repeat(5, [](int i) { std::cout << i << " "; });
std::function作为参数,适合需要把回调存起来的场景。比如事件系统、异步任务队列。
void register_handler(std::function<void(int)> handler) {
// 把handler存到容器里
handlers.push_back(std::move(handler));
}
C++20的auto参数是语法糖,本质还是模板。我个人觉得可读性更好,但团队里有人觉得隐式模板不够明确。看你们项目风格吧。
6.4 存储Lambda的实战场景
实际项目中,Lambda的存储和传递往往不是孤立的。我整理了几个常见模式:
6.4.1 回调容器
class Button {
std::vector<std::function<void()>> click_handlers_;
public:
void on_click(std::function<void()> handler) {
click_handlers_.push_back(std::move(handler));
}
void click() {
for (auto& h : click_handlers_) {
h();
}
}
};
这里必须用std::function,因为容器需要统一类型。auto做不到。
6.4.2 延迟执行
std::function<int()> create_adder(int x) {
return [x](int y) { return x + y; };
}
auto add5 = create_adder(5);
std::cout << add5(10) << std::endl; // 15
返回Lambda时,返回值类型必须明确。std::function是自然的选择。auto推导在返回类型上不好用——除非你用decltype,但太啰嗦了。
6.4.3 策略模式
class Sorter {
std::function<bool(int, int)> comp_;
public:
void set_comparator(std::function<bool(int, int)> comp) {
comp_ = std::move(comp);
}
void sort(std::vector<int>& data) {
std::sort(data.begin(), data.end(), comp_);
}
};
运行时切换排序策略,std::function是标配。用模板的话,你得把Sorter也变成模板类,灵活性就差了。
6.5 性能取舍——我的一些经验
很多人担心std::function的性能。说实话,大部分场景下那点开销可以忽略。但如果你写的是高性能计算、游戏引擎、高频交易系统,就得认真对待了。
我做过一个测试:调用1000万次空Lambda,模板版本比std::function快大约2-3倍。原因在于std::function内部有虚函数调用,而且可能涉及小对象优化(SBO)或堆分配。
我的建议是:
- 默认用模板参数,除非你需要类型擦除
- 需要存储回调时,用std::function,但注意捕获的变量不要太大(超过SBO缓冲区会触发堆分配)
- 如果Lambda捕获了很多变量,考虑用std::shared_ptr包装数据,或者改用函数对象
6.6 本章知识体系
下面这张图梳理了Lambda存储与传递的核心脉络:
说白了,选择哪种方式取决于你的需求:要性能选auto或模板,要灵活选std::function。没有银弹,只有权衡。我在项目中通常这样决策:如果Lambda只在局部用,auto搞定;如果需要跨函数传递或存储,先试试模板,不行再上std::function。
记住一点:Lambda本身是轻量的,但捕获的变量和存储方式会影响性能。多想想生命周期,多想想类型擦除的代价。嗯,这些坑我都踩过,希望你能少走弯路。