关于C++使用lambda表达式写递归函数时的踩坑记录
关于C++使用lambda表达式写递归函数时的踩坑记录
是笔者在写129. 求根节点到叶节点数字之和 - 力扣(LeetCode)这道题时遇到的
问题描述
以下代码是跑不通的,在第14行即dfs调用时会发现dfs未定义
1 | class Solution { |
出现问题的原因
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
通过以下方式解决此问题:
- 先声明后绑定:
std::function dfs
先声明,随后通过赋值操作绑定到 Lambda。 - 类型擦除调用: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也可以