20. 高阶函数:返回 Lambda 的函数,闭包工厂模式
说实话,刚接触 C++ Lambda 那会儿,我总觉得它就是个语法糖——写个匿名函数,省得再单独定义个仿函数。直到我在项目中遇到一个场景:需要根据不同的配置参数,生成一系列行为相似但细节不同的处理函数。那时候我才意识到,Lambda 真正的威力,在于它可以被「生产」出来。
今天我们就聊聊这个——返回 Lambda 的函数,也就是所谓的「高阶函数」。说白了,就是函数也能当工厂,专门生产别的函数。
20.1 什么是高阶函数?
高阶函数这个概念,在函数式编程里很常见。它要么接受函数作为参数,要么返回一个函数。C++ 里,我们主要关注后者——返回一个可调用对象。
你想想看,如果一段逻辑只有参数不同,其他都一样,那何必每次都手写一遍?写个工厂函数,把变化的部分作为参数传进去,让它帮你生成对应的 Lambda,多清爽。
核心思想: 高阶函数 = 函数工厂。输入配置,输出行为。
20.2 闭包工厂模式
闭包工厂,说白了就是「捕获外部变量并返回 Lambda 的函数」。每次调用工厂,都会生成一个独立的闭包,闭包里保存了当时捕获的变量副本。
我在项目中遇到过这样一个需求:需要为不同的传感器生成不同的数据过滤函数。每个传感器的阈值不一样,但过滤逻辑完全一样。用闭包工厂,几行代码就搞定了。
#include <iostream>
#include <functional>
#include <vector>
// 闭包工厂:生成一个比较函数
auto makeComparator(int threshold) {
// 返回的 Lambda 捕获了 threshold
return [threshold](int value) -> bool {
return value > threshold;
};
}
int main() {
// 生产两个不同的比较器
auto cmp1 = makeComparator(10);
auto cmp2 = makeComparator(50);
std::cout << cmp1(20) << std::endl; // 1 (20 > 10)
std::cout << cmp2(20) << std::endl; // 0 (20 < 50)
return 0;
}
你看,makeComparator 就是个工厂。每次调用,它都生成一个全新的闭包,闭包里保存着当时的 threshold 值。两个闭包互不干扰。
20.3 捕获方式的影响
这里有个坑,我曾经踩过。捕获方式不同,闭包的行为可能天差地别。
| 捕获方式 | 行为 | 典型场景 |
|---|---|---|
| 按值捕获 [=] | 拷贝一份,闭包独立 | 工厂函数返回后,变量已销毁 |
| 按引用捕获 [&] | 引用外部变量,需保证生命周期 | 变量生命周期比闭包长 |
| 移动捕获 [x = std::move(v)] | 转移所有权,避免拷贝 | 管理独占资源(如 unique_ptr) |
我曾经犯过的错: 在工厂函数里用 [&] 捕获局部变量,结果函数返回后,局部变量销毁了,闭包变成了悬空引用。程序跑起来时好时坏,查了半天才发现是引用捕获的问题。
所以我的建议是:闭包工厂里,默认用按值捕获。除非你非常确定引用对象的生命周期,否则别冒险。
20.4 返回 std::function 还是 auto?
嗯,这里要注意。Lambda 的类型是匿名的,你没法直接写出来。所以返回时有两种选择:
- 用
auto推导返回类型:C++14 开始支持,简洁高效。但只能在函数定义里用,不能用于头文件声明。 - 用
std::function包装:类型擦除,可以写在头文件里。但会有轻微的性能开销。
// 方式一:auto 推导(推荐,性能好)
auto makeAdder(int x) {
return [x](int y) { return x + y; };
}
// 方式二:std::function(可声明,有开销)
std::function<int(int)> makeAdder2(int x) {
return [x](int y) { return x + y; };
}
我个人习惯:内部实现用 auto,对外接口用 std::function。这样既保证了性能,又保持了接口的清晰。
20.5 实战:策略模式 + 闭包工厂
闭包工厂最实用的场景,就是配合策略模式。我在一个数据处理框架里就这么用过。
#include <iostream>
#include <functional>
#include <vector>
#include <string>
// 策略工厂:根据策略名称生成对应的处理函数
auto createProcessor(const std::string& strategy) {
if (strategy == "double") {
return [](int x) { return x * 2; };
} else if (strategy == "square") {
return [](int x) { return x * x; };
} else {
// 默认策略:原样返回
return [](int x) { return x; };
}
}
int main() {
auto proc = createProcessor("square");
std::vector<int> data = {1, 2, 3, 4, 5};
for (auto& v : data) {
v = proc(v);
}
for (auto v : data) {
std::cout << v << " "; // 1 4 9 16 25
}
return 0;
}
你看,策略的选择被封装在工厂函数里。调用方只需要告诉工厂「我要什么策略」,工厂就给你一个对应的 Lambda。代码干净,扩展也方便——加个新策略,只要在工厂里加个分支就行。
20.6 闭包工厂的陷阱
最后聊几个我踩过的坑:
- 捕获大型对象:按值捕获会拷贝整个对象。如果对象很大(比如一个大容器),性能会受影响。可以考虑用
std::shared_ptr共享所有权。 - 递归 Lambda:Lambda 默认不能递归,因为捕获时自身还没定义。可以用
std::function包装,或者用 Y 组合子(不过后者太学术,不推荐)。 - 默认捕获 [=] 的陷阱:在成员函数里用 [=],会隐式捕获
this指针。如果对象被销毁,闭包就悬空了。建议显式捕获this或者按值捕获成员变量。
小技巧: 如果闭包工厂返回的 Lambda 需要修改捕获的变量,记得加上 mutable 关键字。否则编译器会报错。
auto makeCounter() {
int count = 0;
return [count]() mutable { return ++count; };
// 注意:mutable 允许修改按值捕获的 count
}
20.7 知识体系图
下面这张图,帮你理清本章的核心脉络:
闭包工厂模式,说白了就是「用函数生产函数,用闭包封装状态」。它让代码更灵活,也更可复用。你在项目中遇到「同一套逻辑,不同参数」的场景时,不妨试试这个模式。
公众号:蓝海资料掘金营,微信 deep3321