C++ 中如何优雅地返回一个递归闭包函数?

在刷Leetcode时,我遇到了一道题目(详见Leetcode 第426场周赛分析总结Q3),需要对两棵树建图,然后以每个节点作为根节点进行DFS遍历。一般的实现方法是将重复的逻辑封装起来,写两个函数,一个负责建图,另一个负责DFS,然后将建图后的返回值作为参数传递给DFS。

在Python、JavaScript等高级语言中,有一种叫做闭包函数的编程技巧,能够简化这种逻辑。闭包本质上是一个函数A返回另一个函数B,而函数B捕获了函数A的局部变量,从而使函数B拥有状态信息。在C++中,我们也可以利用闭包函数,将建图与DFS的逻辑绑定,从而避免显式地在调用时传递参数。

以下是该问题的C++实现:

class Solution {
public:vector<int> maxTargetNodes(vector<vector<int>>& edges1, vector<vector<int>>& edges2, int k) {// 建立一个闭包函数,用于返回DFS函数auto get_dfs = [](decltype(edges1) edges) -> auto {int n = edges.size() + 1;vector graph(n, vector<int>());for (auto &&edge : edges) {auto u = edge[0], v = edge[1];graph[u].push_back(v);graph[v].push_back(u);}function<int(int, int, int)> dfs;dfs = [graph = std::move(graph), &dfs](int u, int fa, int d) -> int {if (d < 0) return 0;int ans = 1;for (auto v : graph[u]) {if (v == fa) continue;ans += dfs(v, u, d - 1);}return ans;};return dfs;};// 对第二棵树进行DFS,计算其最大目标节点数int maxn2 = 0;if (k > 0) {auto dfs = get_dfs(edges2);int n = edges2.size() + 1;for (int i = 0; i < n; ++i) {maxn2 = max(maxn2, dfs(i, -1, k - 1));}}// 对第一棵树进行DFS,结合第二棵树的结果计算答案auto dfs = get_dfs(edges1);int n = edges1.size() + 1;vector<int> ans(n, 0);for (int i = 0; i < n; ++i) {ans[i] = dfs(i, -1, k) + maxn2;}return ans;}
};

闭包函数的核心原理

在C++中,闭包(Closure)是通过lambda表达式实现的一种语法特性。闭包可以捕获外部上下文中的变量,将其绑定到返回的函数中,从而避免显式传递参数。在上面的代码中,get_dfs函数返回了一个DFS函数,这个DFS函数通过捕获将graph变量绑定到其作用域中。

以下是闭包函数的几项核心要点:

  1. 变量捕获

    • 值捕获(by value):将外部变量的值拷贝到闭包中,闭包对这些变量的修改不会影响外部变量。
    • 引用捕获(by reference):闭包捕获外部变量的引用,闭包对变量的修改会影响外部变量。
    • C++14后的移动捕获(move capture):通过std::move,可以将大对象的所有权转移到闭包中,避免拷贝的开销。
  2. 闭包的生命周期

    • 在C++中,闭包的生命周期由捕获的变量决定。如果捕获的变量是局部变量,需确保这些变量在闭包的使用过程中始终有效。

实现中的关键点

1. 移动捕获graph

get_dfs函数中,我们通过std::move(graph)将图的所有权转移到闭包中:

dfs = [graph = std::move(graph), &dfs](int u, int fa, int d) -> int { ... };

为什么要使用移动捕获?

  • 如果使用值捕获,graph会被拷贝,可能造成性能开销,特别是在graph较大时。
  • 如果使用引用捕获,get_dfs函数返回后,graph的生命周期结束,闭包中的引用将变为悬挂指针,导致未定义行为。
  • 移动捕获通过转移所有权将graph绑定到闭包中,使其生命周期与闭包一致,既避免了拷贝开销,又保证了安全性。
  • 需要注意的是,移动捕获会使闭包与捕获的资源绑定,可能导致资源生命周期难以管理。在更复杂的场景下,可以考虑将 graph 提前封装到一个辅助类中,避免直接捕获大对象。
2. 引用捕获自身dfs

在递归的实现中,dfs函数需要捕获自身。这通过引用捕获实现:

dfs = [graph = std::move(graph), &dfs](int u, int fa, int d) -> int { ... };

如果进行值捕获([dfs]),编译器会尝试拷贝 dfs,但在捕获时 dfs 仍未完全定义(不完全类型),因此会报错。

为什么捕获dfs可以使用引用?

  • 返回闭包时,dfs作为函数的返回值,在返回值优化(RVO)的作用下,其内存不会被销毁。因此,引用捕获dfs是安全的。
  • RVO 是一种编译器优化技术,用于避免对象的临时拷贝或移动。其核心思想是:在函数返回对象时,直接在调用方的内存中构造返回值,跳过临时对象的拷贝或移动操作
  • 需要注意的是,在不支持 RVO 的情况下,引用捕获 dfs 是不安全的。这时就需要把dfs作为参数进行传递,或者在内层再使用std::function进行包裹。

闭包的优势与局限性

优势
  1. 代码简洁:将建图和DFS逻辑封装在一个闭包中,避免显式传递参数。
  2. 状态绑定:闭包通过捕获机制,将graph与DFS逻辑绑定,减少上下文管理的复杂性。
  3. 灵活性:闭包函数可以作为返回值,方便以声明式方式组织代码。
局限性
  1. 复杂性增加:闭包的捕获规则较为灵活,但也容易出现因误用而导致的悬挂引用或性能问题。
  2. 调试困难:闭包在捕获变量时会隐式生成代码,可能导致调试困难。
  3. 性能开销:虽然可以通过移动捕获减少拷贝,但闭包仍可能引入额外的性能开销,特别是当捕获大量对象时。

总结与反思

通过闭包函数,将建图与DFS逻辑绑定,简化了调用接口,同时减少了显式参数传递的麻烦。这种高级技巧在C++中并不常见,但在特定场景(小型、局部的递归场景)下能够显著提升代码的可读性与复用性。

然而,闭包函数的使用也需要谨慎,特别是在C++中,变量的捕获方式直接影响代码的安全性与性能。通过对捕获规则(值捕获、引用捕获、移动捕获)的深入理解,可以更安全、高效地使用闭包,提高代码质量。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/503566.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Mysql--基础篇--数据类型(整数,浮点数,日期,枚举,二进制,空间类型等)

MySQL提供了多种数据类型&#xff0c;用于定义表中列的数据格式。选择合适的数据类型不仅可以提高查询性能&#xff0c;还能确保数据的完整性和准确性。 一、数值类型 数值类型用于存储整数、浮点数和定点数。根据精度和范围的不同&#xff0c;数值类型可以分为以下几类&…

nlp培训重点-2

1. 贝叶斯公式 import math import jieba import re import os import json from collections import defaultdictjieba.initialize()""" 贝叶斯分类实践P(A|B) (P(A) * P(B|A)) / P(B) 事件A&#xff1a;文本属于类别x1。文本属于类别x的概率&#xff0c;记做…

sunrays-framework(太阳射线框架搭建)

文章目录 1.基本环境搭建1.创建项目sunrays-framework2.新增忽略文件3.删除src目录4.交给Git管理 2.sunrays-dependencies模块&#xff1a;统一管理依赖1.创建模块2.不要交给父模块管理&#xff0c;这是独立的&#xff01;&#xff01;&#xff01;3.删除src目录4.pom.xml统一管…

JVM vs JDK vs JRE

JVM是Java虚拟机的缩写&#xff0c; 用于实现Java的一次编译&#xff0c;处处运行。 Java代码写成.class后&#xff0c;由本地的虚拟机运行。 JDK&#xff08;Java Development Kit&#xff09;是一个功能齐全的 Java 开发工具包&#xff0c;供开发者使用。 JDK包含了JRE。…

Android修改开机动画路径

frameworks\base\cmds\bootanimation\BootAnimation.cpp 路径的定义 优先查找的顺序

select下拉框,首次进入页面没有显示value的情况

bug场景&#xff1a; 类似这种bug情况排查如下&#xff1a; 首先 理解含义 options就是存放键值对的&#xff0c;id就是key&#xff0c;对上了它就自动把label显示 而且如果你用来当作key和label的字段&#xff0c;与后端返回的不一致&#xff0c;还可以进行更改 其次 排查接…

Redis中的主从/Redis八股

四、Redis主从 1.搭建主从架构 不像是负载均衡&#xff0c;这里是主从&#xff0c;是因为redis大多数是读少的是写 步骤 搭建实例&#xff08;建设有三个实例&#xff0c;同一个ip不同端口号&#xff09; 1&#xff09;创建目录 我们创建三个文件夹&#xff0c;名字分别叫700…

Mysql--基础篇--函数(字符串函数,日期函数,数值函数,聚合函数,自定义函数及与存储过程的区别等)

MySQL提供了丰富的内置函数&#xff0c;涵盖了字符串处理、数值计算、日期和时间操作、聚合统计、控制流等多种功能。这些函数可以帮助你简化SQL查询&#xff0c;提升开发效率。 除了内置函数&#xff0c;MySQL还支持自定义函数&#xff08;User-Defined Functions&#xff09;…

【linux系统之redis6】redis的安装与初始化

下载redis的linux对应的安装包&#xff0c;并上传到linux虚拟机里面 解压压缩包 tar -zxzf redis-6.2.6.tar.gz解压后&#xff0c;进入redis文件 cd redis-6.2.6执行编译 make && make install看到下图&#xff0c;就说明redis安装成功了 默认的安装路径&#xff0c…

怎么管理电脑usb接口,分享四种USB端口管理方法

怎么管理电脑usb接口&#xff0c;分享四种USB端口管理方法 USB接口作为电脑重要的外部接口&#xff0c;方便了数据传输和设备连接。 然而&#xff0c;不加管理的USB接口也可能带来安全隐患&#xff0c;例如数据泄露、病毒传播等。 因此&#xff0c;有效管理电脑USB接口至关重…

[开源]自动化定位建图系统

系统状态机&#xff1a; 效果展示&#xff1a; 1、 机器人建图定位系统-基础重定位&#xff0c;定位功能演示 2、 机器人建图定位系统-增量地图构建&#xff0c;手动回环检测演示 3、… 开源链接&#xff1a; https://gitee.com/li-wenhao-lwh/lifelong-backend Qt人机交互…

重新整理机器学习和神经网络框架

本篇重新梳理了人工智能&#xff08;AI&#xff09;、机器学习&#xff08;ML&#xff09;、神经网络&#xff08;NN&#xff09;和深度学习&#xff08;DL&#xff09;之间存在一定的包含关系&#xff0c;以下是它们的关系及各自内容,以及人工智能领域中深度学习分支对比整理。…

PyTorch 框架实现线性回归:从数据预处理到模型训练全流程

系列文章目录 01-PyTorch新手必看&#xff1a;张量是什么&#xff1f;5 分钟教你快速创建张量&#xff01; 02-张量运算真简单&#xff01;PyTorch 数值计算操作完全指南 03-Numpy 还是 PyTorch&#xff1f;张量与 Numpy 的神奇转换技巧 04-揭秘数据处理神器&#xff1a;PyTor…

Elasticsearch:优化的标量量化 - 更好的二进制量化

作者&#xff1a;来自 Elastic Benjamin Trent 在这里&#xff0c;我们解释了 Elasticsearch 中的优化标量量化以及如何使用它来改进更好的二进制量化 (Better Binary Quantization - BBQ)。 我们的全新改进版二进制量化 (Better Binary Quantization - BBQ) 索引现在变得更强大…

科普CMOS传感器的工作原理及特点

在当今数字化成像的时代&#xff0c;图像传感器无疑是幕后的关键 “功臣”&#xff0c;它宛如一位神奇的 “光影魔法师”&#xff0c;通过光电效应这一奇妙的物理现象&#xff0c;将光子巧妙地转换成电荷&#xff0c;为图像的诞生奠定基础。而在众多类型的图像传感器中&#xf…

IDEA中Maven依赖包导入失败报红的潜在原因

在上网试了别人的八个问题总结之后依然没有解决&#xff1a; IDEA中Maven依赖包导入失败报红问题总结最有效8种解决方案_idea导入依赖还是报红-CSDN博客https://blog.csdn.net/qq_43705131/article/details/106165960 江郎才尽之后突然想到一个原因&#xff1a;<dep…

Java100道面试题

1.JVM内存结构 1. 方法区&#xff08;Method Area&#xff09; 方法区是JVM内存结构的一部分&#xff0c;用于存放类的相关信息&#xff0c;包括&#xff1a; 类的结构&#xff08;字段、方法、常量池等&#xff09;。字段和方法的描述&#xff0c;如名称、类型、访问修饰符…

虚表 —— 隐藏行(简单版)

因为隐藏行改变了listview内部行号处理机制&#xff0c;需要处理大量细节&#xff0c;如listview内部用于传递行号的各种消息、通知等、封装的各种读取行号的函数等。 所以在工作量很大&#xff0c;一处纰漏可能导致重大bug的情况下&#xff0c;仅对隐藏行功能进行了简单封装&…

UDP -- 简易聊天室

目录 gitee&#xff08;内有详细代码&#xff09; 图解 MessageRoute.hpp UdpClient.hpp UdpServer.hpp Main.hpp 运行结果&#xff08;本地通信&#xff09; 如何分开对话显示&#xff1f; gitee&#xff08;内有详细代码&#xff09; chat_room zihuixie/Linux_Lear…

python制作翻译软件

本文复刻此教程&#xff1a;制作属于自己的翻译软件-很简单【Python】_哔哩哔哩_bilibili 一、明确需求&#xff08;以搜狗翻译为例&#xff09; &#xff08;1&#xff09;网址&#xff1a;https://fanyi.sogou.com/text &#xff08;2&#xff09; 数据&#xff1a;翻译内容…