21. 递归 Lambda:C++14 中的泛型 Lambda 实现递归
递归,说白了就是函数调用自己。这在普通函数里很自然,但到了 Lambda 这儿,事情就变得有点微妙了。我记得第一次在项目里试图写一个递归 Lambda 时,编译器直接给我甩了一脸错误——"变量 'f' 在初始化之前无法使用"。嗯,这确实是个坑。
为什么 Lambda 不能直接递归?
先看一个最简单的例子。假设我们要写一个计算阶乘的递归 Lambda:
auto factorial = [](int n) {
return n <= 1 ? 1 : n * factorial(n - 1); // 编译错误!
};
为什么会报错?因为 Lambda 表达式在定义时,右侧的 factorial 变量还没完全构造好。你想想看,编译器看到 auto factorial = ... 时,它需要先知道 factorial 的类型,才能去解析 Lambda 体里的 factorial 调用。这就成了「先有鸡还是先有蛋」的问题。
我当年刚接触 Lambda 时,第一反应是「用 std::function 包一层不就行了?」确实可以,但代价不小。
方案一:std::function 包装(C++11 可用)
#include <functional>
std::function<int(int)> factorial = [&](int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
};
// 使用
int result = factorial(5); // 120
这里的关键是 [&] 捕获了外部的 factorial 引用。因为 std::function 是个类型擦除的包装器,它的构造在赋值语句完成时就结束了,所以 Lambda 体里可以安全地引用它。
std::function 的虚函数派发,比直接函数调用慢不少。我在一个高频交易系统的回测模块里用过这种写法,结果性能瓶颈就卡在这里——递归深度一上来,延迟直接翻倍。
方案二:C++14 泛型 Lambda + 自传递参数
C++14 引入了泛型 Lambda,也就是可以用 auto 作为参数类型。这给了我们一个更优雅的解法:把 Lambda 自己作为参数传进去。
auto factorial = [](auto&& self, int n) -> int {
return n <= 1 ? 1 : n * self(self, n - 1);
};
// 调用时要把自己传进去
int result = factorial(factorial, 5); // 120
这个写法有点绕,但核心思想很简单:Lambda 的第一个参数接收自身,然后在递归时把 self 再次传给自己。说白了,就是「手动实现递归闭包」。
我个人习惯把这种模式叫做「Y 组合子的 C++ 亲戚」。虽然名字吓人,但实际用起来就两行代码的事。
方案三:Y 组合子封装(推荐)
每次调用都要传自己,太丑了。我们可以封装一个工具函数,把「自传递」的逻辑藏起来:
#include <functional>
template <typename F>
auto y_combinator(F f) {
return [f](auto&&... args) {
return f(f, std::forward<decltype(args)>(args)...);
};
}
// 使用
auto factorial = y_combinator([](auto&& self, int n) -> int {
return n <= 1 ? 1 : n * self(self, n - 1);
});
int result = factorial(5); // 120,现在不用传自己了
这个 y_combinator 把「传自己」这一步自动化了。你只需要写递归逻辑,剩下的交给模板搞定。
self(self, ...) 的写法太丑,可以用 auto&& self 配合 decltype 做一层转发。我在一个开源项目里见过有人用宏包装,但说实话,宏的调试体验太差了,不推荐。
方案四:C++23 的 Deducing This(前瞻)
C++23 引入了一个新特性叫「Deducing This」,允许 Lambda 显式声明 this 参数。这样一来,递归 Lambda 可以写成:
auto factorial = [](this auto&& self, int n) -> int {
return n <= 1 ? 1 : n * self(n - 1);
};
int result = factorial(5); // 120,最自然的写法
注意看,这里的 self 不再需要手动传递了,编译器会自动处理。这是目前最优雅的递归 Lambda 写法,可惜需要 C++23 编译器支持。
知识体系总览
下面这张图总结了递归 Lambda 的几种实现方式及其适用场景:
避坑指南
我曾经在一个项目里用 std::function 方案写了一个深度优先搜索的递归 Lambda,结果递归深度到了 1000 层时,程序直接栈溢出了。排查了半天才发现,std::function 的每次递归调用都会在堆上分配内存,加上栈帧本身,内存消耗比普通递归函数大了好几倍。
所以我的建议是:
- 性能敏感场景:用 Y 组合子或自传递参数方案,零额外开销
- 快速原型:
std::function方案够用,但注意递归深度 - 新项目:如果编译器支持 C++23,直接用 Deducing This
std::function 虽然方便,但性能上要打个折扣。
嗯,递归 Lambda 这个话题就聊到这儿。下次你在代码里遇到需要递归的 Lambda 场景,应该知道怎么选了。
公众号:蓝海资料掘金营,微信deep3321