在 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
}

性能排序(通常情况)

  1. 🥇 C++23 this auto / 普通递归函数 - 最快,零开销
  2. 🥈 Y 组合子 - 接近零开销,编译器易内联
  3. 🥉 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 递归会和普通函数递归一样自然。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐