C++ Lambda 中实现递归
·
在 C++ Lambda 中实现递归
Lambda 表达式本质是匿名函数对象,没有自己的名字,这让递归变得有些棘手。但实际上有多种方法可以实现 Lambda 递归,每种都有其适用场景。
一、为什么 Lambda 递归很特殊?
普通函数递归很简单
// 普通函数:自己调用自己,天经地义
int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
Lambda 的困境
// ❌ 编译错误:lambda 没有名字,无法在内部引用自己
auto factorial = [](int n) {
return n <= 1 ? 1 : n * factorial(n - 1); // 错误!factorial 未定义
};
Lambda 的 auto 类型推导需要在初始化完成后才知道类型,但在 Lambda 体内部使用时,推导还未完成——这就是先有鸡还是先有蛋的问题。
二、方法一:std::function(最简单)
最直接的方法,利用 std::function 的类型擦除能力。
基础用法
#include <functional>
#include <iostream>
std::function<int(int)> factorial = [&](int n) -> int {
return n <= 1 ? 1 : n * factorial(n - 1);
};
std::cout << factorial(5) << std::endl; // 120
关键点:
- 必须使用
[&]捕获,因为需要引用外部的std::function对象 - 必须显式声明
std::function<int(int)>类型 - 必须显式指定返回类型
-> int
经典示例:斐波那契数列
std::function<int(int)> fib = [&](int n) -> int {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
};
// 打印前10个斐波那契数
for (int i = 0; i < 10; ++i) {
std::cout << fib(i) << " "; // 0 1 1 2 3 5 8 13 21 34
}
优缺点
✅ 优点:
- 代码清晰,易于理解
- 支持闭包,可以捕获外部变量
❌ 缺点:
std::function有运行时开销(类型擦除、可能的堆分配)- 性能敏感场景不适合
三、方法二:Y 组合子(纯函数式风格)
Y 组合子是一种无需赋值就能实现递归的函数式编程技巧,让 Lambda 递归不依赖外部变量。
基础实现
// Y 组合子:一个高阶函数,赋予匿名函数递归能力
template<typename F>
struct YCombinator {
F f;
template<typename... Args>
auto operator()(Args&&... args) const {
return f(*this, std::forward<Args>(args)...);
}
};
// C++17 CTAD 推导指引
template<typename F>
YCombinator(F) -> YCombinator<F>;
使用方式
// 阶乘:需要额外参数来传递"自己"
auto factorial = YCombinator{
[](auto&& self, int n) -> int {
return n <= 1 ? 1 : n * self(n - 1);
}
};
std::cout << factorial(5) << std::endl; // 120
更复杂的例子:树遍历
#include <iostream>
struct TreeNode {
int value;
TreeNode* left;
TreeNode* right;
};
// 使用 Y 组合子实现中序遍历
auto inorder = YCombinator{
[](auto&& self, TreeNode* node) -> void {
if (!node) return;
self(node->left);
std::cout << node->value << " ";
self(node->right);
}
};
// 使用
TreeNode* root = /* ... */;
inorder(root);
C++14 简洁版(使用泛型 Lambda)
auto y_combinator = [](auto f) {
return [f](auto... args) {
return f(f, args...);
};
};
// 使用
auto factorial = y_combinator(
[](auto self, int n) -> int {
return n <= 1 ? 1 : n * self(self, n - 1);
}
);
std::cout << factorial(5) << std::endl; // 120
优缺点
✅ 优点:
- 纯函数式,不依赖外部状态
- 可以在任何地方使用,包括全局初始化
- 性能优于
std::function
❌ 缺点:
- 语法略显晦涩
- 每次调用需要传递
self参数
三、方法三:C++23 的显式 this 参数(未来方案)
C++23 引入了显式对象参数,让 Lambda 递归变得极其自然。
语法(需 C++23 支持)
// 使用 this 作为显式参数,lambda 可以自我引用
auto factorial = [](this auto&& self, int n) -> int {
return n <= 1 ? 1 : n * self(n - 1);
};
std::cout << factorial(5) << std::endl; // 120
更自然的递归
// 斐波那契:和普通函数一样直观
auto fib = [](this auto&& self, int n) -> int {
if (n <= 1) return n;
return self(n - 1) + self(n - 2);
};
// 树遍历
auto traverse = [](this auto&& self, TreeNode* node) -> void {
if (!node) return;
self(node->left);
std::cout << node->value << " ";
self(node->right);
};
优缺点
✅ 优点:
- 语法自然,和普通递归函数几乎一样
- 性能最优,无额外开销
- 支持完美转发
❌ 缺点:
- 需要 C++23 标准支持(编译器支持尚不完善)
- 目前主流项目还无法使用
四、方法四:使用函数指针(有限制)
如果 Lambda 不捕获任何变量,可以转换为函数指针实现递归。
// 声明函数指针,然后用 lambda 赋值
int (*factorial)(int) = nullptr;
factorial = [](int n) -> int {
return n <= 1 ? 1 : n * factorial(n - 1);
};
std::cout << factorial(5) << std::endl; // 120
更安全的方式(立即初始化)
auto factorial = [](int n) -> int {
// 静态局部变量,指向 lambda 的函数指针
static auto impl = +[](int n) -> int {
// 这里不能直接用 factorial,因为作用域问题
return n <= 1 ? 1 : n * factorial(n - 1);
};
return impl(n);
};
优缺点
✅ 优点:
- 无
std::function的运行时开销
❌ 缺点:
- 不能捕获外部变量(只能是无状态 Lambda)
- 语法别扭,容易出错
五、性能对比实测
#include <chrono>
#include <iostream>
// 测试不同方法的性能
int main() {
const int N = 30;
const int ITERATIONS = 100000;
// 1. std::function 方式
std::function<int(int)> fib1;
fib1 = [&](int n) -> int {
return n <= 1 ? n : fib1(n - 1) + fib1(n - 2);
};
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < ITERATIONS; ++i) fib1(N);
auto end = std::chrono::high_resolution_clock::now();
std::cout << "std::function: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
<< "ms\n";
// 2. Y 组合子方式
auto yfib = YCombinator{[](auto&& self, int n) -> int {
return n <= 1 ? n : self(n - 1) + self(n - 2);
}};
start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < ITERATIONS; ++i) yfib(N);
end = std::chrono::high_resolution_clock::now();
std::cout << "Y Combinator: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
<< "ms\n";
// 3. 普通函数
auto normalfib = [](int n) {
std::function<int(int)> fib = [&](int n) -> int {
return n <= 1 ? n : fib(n - 1) + fib(n - 2);
};
return fib(n);
};
// 典型结果(取决于编译器优化):
// std::function: ~1200ms
// Y Combinator: ~600ms
// 普通函数: ~500ms
}
性能排序(通常情况):
- 🥇 C++23
this auto/ 普通递归函数 - 最快,零开销 - 🥈 Y 组合子 - 接近零开销,编译器易内联
- 🥉 std::function - 有明显开销(虚函数调用、堆分配)
六、实际应用场景
场景一:缓存中间结果的递归
// 使用 std::function + 外部 map 实现记忆化
std::function<int(int)> memo_fib = [&](int n) -> int {
static std::map<int, int> cache;
if (n <= 1) return n;
auto it = cache.find(n);
if (it != cache.end()) return it->second;
return cache[n] = memo_fib(n - 1) + memo_fib(n - 2);
};
std::cout << memo_fib(50) << std::endl; // 瞬间完成!
场景二:目录树遍历
#include <filesystem>
namespace fs = std::filesystem;
auto traverse_directory = [](const fs::path& dir) {
std::function<void(const fs::path&)> impl;
impl = [&](const fs::path& current) {
for (const auto& entry : fs::directory_iterator(current)) {
std::cout << entry.path() << std::endl;
if (entry.is_directory()) {
impl(entry.path()); // 递归进入子目录
}
}
};
impl(dir);
};
traverse_directory("/path/to/directory");
场景三:递归数据结构处理
// JSON 解析器中的递归处理
auto parse_value = [](const std::string& json) -> JSONValue {
std::function<JSONValue(const std::string&)> parse;
parse = [&](const std::string& str) -> JSONValue {
if (str[0] == '{') {
JSONObject obj;
// 解析嵌套对象时递归调用 parse
// ...
return obj;
} else if (str[0] == '[') {
JSONArray arr;
// 解析数组元素时递归调用 parse
// ...
return arr;
}
return parse_primitive(str);
};
return parse(json);
};
七、选择指南
| 方法 | 适用场景 | 性能 | 可读性 |
|---|---|---|---|
| std::function | 快速原型、非性能敏感代码 | ⭐⭐ | ⭐⭐⭐⭐⭐ |
| Y 组合子 | 函数式风格、性能敏感 | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| C++23 this | 未来项目、追求完美 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 函数指针 | 仅无状态 Lambda | ⭐⭐⭐⭐ | ⭐⭐ |
我的推荐
// 日常开发首选:std::function,简单直接
std::function<int(int)> factorial = [&](int n) -> int {
return n <= 1 ? 1 : n * factorial(n - 1);
};
// 性能敏感或炫技:Y 组合子
auto factorial = YCombinator{[](auto&& self, int n) -> int {
return n <= 1 ? 1 : n * self(n - 1);
}};
// 注意:如果递归逻辑复杂、多处调用,直接写成普通函数更合适!
八、常见陷阱
❌ 陷阱一:忘记引用捕获
// 错误:值捕获导致递归调用的是 std::function 的副本
std::function<int(int)> factorial = [=](int n) -> int { // 错误!应该是 [&]
return n <= 1 ? 1 : n * factorial(n - 1);
};
❌ 陷阱二:auto 类型推导失败
// 错误:auto 无法推导递归 lambda 的类型
auto factorial = [&](int n) { // 类型不完整
return n <= 1 ? 1 : n * factorial(n - 1);
};
// 修正:必须显式指定 std::function 类型
❌ 陷阱三:Y 组合子的生命周期
// 危险:捕获了临时对象
auto make_factorial = []() {
int base = 100;
return YCombinator{[&base](auto&& self, int n) -> int {
// base 可能已经失效!
return n <= 1 ? base : n * self(n - 1);
}};
};
auto factorial = make_factorial(); // base 已销毁!
最终建议:除非有明显的性能瓶颈,否则优先使用 std::function 实现 Lambda 递归。代码的清晰性远比微小的性能差异重要。等到 C++23 普及,直接使用 this auto 参数,届时 Lambda 递归会和普通函数递归一样自然。
更多推荐




所有评论(0)