关于C++使用lambda表达式写递归函数时的踩坑记录

是笔者在写129. 求根节点到叶节点数字之和 - 力扣(LeetCode)这道题时遇到的

问题描述

以下代码是跑不通的,在第14行即dfs调用时会发现dfs未定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
int sumNumbers(TreeNode* root) {
int ans = 0;
auto dfs = [&](TreeNode* node, int x) -> void {
if (node == nullptr) {
return;
}
x = x * 10 + node->val;
if (node->left == node->right) { // node 是叶子节点
ans += x;
return;
}
dfs(node->left, x);
dfs(node->right, x);
};
dfs(root, 0);
return ans;
}
};

出现问题的原因

1
auto dfs = [&](TreeNode* node, int x) -> void

普通 lambda 表达式无法直接引用自身,因为 dfs 的声明尚未完成(变量未初始化前无法使用)

编译错误:若在 lambda 内部调用 dfs(...),编译器会报错“dfs 未定义”或“无法捕获自身”。

普通 lambda 的局限性

第二个 lambda 的捕获列表 [&] 只能捕获外部变量,但无法捕获自身(因为自身尚未完全定义)。若尝试在内部调用 dfs,会因闭包未完成初始化而失败。

解决措施

1.显式对象参数(推荐)

1
auto dfs = [&](this auto&& dfs, TreeNode* node, int x) -> void
  • 关键点:使用了 显式对象参数(this auto&& dfs)(C++17)
  • 作用:将 lambda 自身作为参数传递给函数对象,使内部能通过 dfs 引用自身,从而实现递归调用。
  • 实现递归的原理:通过模板化的 this 参数,编译器会在实例化时生成一个可以递归调用的闭包类型,直接绑定到当前 lambda 的名称上。

显式对象参数的作用

lambda 通过 this auto&& dfs 显式传递自身,本质上创建了一个 递归闭包。这里的 dfs 是一个模板参数,允许在定义时通过模板实例化完成自引用。这类似于函数指针的递归调用,但利用了模板推导的特性。

2.std::function包装

1
function<void(TreeNode*,string)> dfs=[&](TreeNode* t,string path)->void

疑问:为什么不带对象参数的普通lambda不行,而function包装的却可以呢?

0.概括的说

如果你不想看下面这一大段话的话,那我概括来说:

1
function<void(TreeNode*,string)> dfs=[&](TreeNode* t,string path)->void

前面说了没有显示对象参数的dfs不可以的原因是dfs内部不可以调用没有定义的dfs。但这种形式的dfs这个变量在使用lambda表达式之前,已经被function这个类型初始化并且给定义好了,所以在lambda中就可以正常调用dfs这个函数对象。

下面是具体的原因叙述:

核心原因在于 std::function 的类型擦除特性Lambda 的捕获机制的结合。以下是具体分析:

1. std::function 的包装作用

  • 类型擦除:std::function 是一个通用的可调用对象包装器,能够存储任何符合签名的可调用对象(如普通函数、成员函数、Lambda 等)。它通过类型擦除技术隐藏了底层具体类型,仅暴露统一的调用接口。
  • 延迟绑定:当 Lambda 被赋值给std::function对象(如dfs)时,Lambda 的实例已经完成初始化。std::function在内部保存了 Lambda 的副本或引用,从而允许通过tra间接调用自身

2. Lambda 的捕获机制

  • 引用捕获 [&]:Lambda 通过[&]捕获外部作用域的变量(包括dfs自身)。此时,Lambda 内部可以通过捕获的dfs引用调用已经初始化的std::function对象,实现递归

  • 生命周期保障:由于std::function 对象dfs 在 Lambda 定义之前已声明,Lambda 捕获的是外部作用域的

    dfs,而tra在赋值时已经绑定到 Lambda 实例,因此不会出现未初始化的悬垂引用问题

3. 与直接 Lambda 递归的区别

普通 Lambda 无法直接递归调用自身,因为 Lambda 在定义时尚未完成初始化,无法直接引用自身。例如:

1
auto dfs = [&](TreeNode* t) { dfs(t->left); };  // 错误:dfs 未定义

std::function 通过以下方式解决此问题:

  1. 先声明后绑定std::function dfs 先声明,随后通过赋值操作绑定到 Lambda。
  2. 类型擦除调用:Lambda 内部通过捕获的dfs调用已绑定的std::function对象,绕过了直接引用未初始化 Lambda 的问题

4. 性能与实现代价

  • 性能开销:std::function的类型擦除会引入间接调用和动态内存分配,可能导致性能损失(如 Benchmarks 显示其递归调用比直接 Lambda 慢约 2.5 倍)
  • 代码灵活性:尽管性能略低,但std::function 提供了更灵活的递归编写方式,尤其在需要动态绑定不同可调用对象时优势明显

总结

  • std::function + 引用捕获 的方案通过类型擦除和延迟绑定,使得 Lambda 能间接调用自身,解决了递归问题。
  • 该方法的代价是性能开销,但在旧版 C++ 标准(C++11/14)中是常用方案
  • 若使用 C++17 或更高版本,优先选择显式对象参数语法,兼顾效率与简洁性

大总结

如果要用 lambda + auto的话,就加上显示对象参数

或者不用auto使用function也可以