当前位置: 首页 > news >正文

掌握C++回调:按值捕获、按引用捕获与弱引用

文章目录

    • 一、按引用捕获和按值捕获
      • 1.1 原理
      • 1.2 案例
    • 二、弱引用
      • 2.1 原理
      • 2.2 案例一
      • 2.3 案例二:使用`base`库的弱引用
    • 三、总结

在C++回调中,当使用Lambda表达式捕获外部变量时,有两种捕获方式:按值捕获和按引用捕获。

一、按引用捕获和按值捕获

1.1 原理

  • 按引用捕获是将外部变量的引用存储在Lambda表达式的闭包中,[&] 表示按引用捕获所有外部变量。这样,当Lambda表达式执行时,它将直接访问原始变量。这种方式在某些情况下可能导致问题,例如,当回调执行时,原始变量已经失效(例如,原始变量是栈上的局部变量,而回调在该变量离开作用域后执行)。

  • 按值捕获是将外部变量的值复制到Lambda表达式的闭包中。这样,当Lambda表达式执行时,它将使用这个复制的值,而不是原始变量的值。这种方式可以避免在回调执行时,原始变量已经失效的问题。

1.2 案例

原理虽然很简单,但是当我们处于复杂的业务代码中时,仍然不免会写出bug。下面是笔者遇到的一个真实案例:

    std::string WebProxyKeysHelper::GetAuthCode(std::string &st_or_code, std::string &last_key) {...auto ph = (st_or_code == KEY_TYPE_OF_ST_STR ? RefreshProxySt() : RefreshOauthCode());auto prom_ptr = std::make_shared<std::promise<std::string>>();std::future<std::string> fut_pb = prom_ptr->get_future();ph.then([&, prom_ptr](bool ret) {std::string tmp_key = "";if (ret) {tmp_key = st_or_code == KEY_TYPE_OF_ST_STR ? proxy_st_ : oauth_code_;UpdateKeys(st_or_code, tmp_key);Schedule();}prom_ptr->set_value(tmp_key);}).onError([&, prom_ptr](const std::exception& ex){prom_ptr->set_value("");});}...return current_key;}

在上述代码中,WebProxyKeysHelper::GetAuthCode 函数通过异步操作 ph 获取代理密钥。然后,根据异步操作的结果,回调函数更新密钥并设置 prom_ptr 的值。然而,这段代码存在一个潜在的问题,即在回调函数中使用了按引用捕获的 st_or_code 变量。

问题在于,当 ph.then([&, prom_ptr](bool ret) { ... }) 回调执行时,st_or_code 变量可能已经离开了作用域并被销毁。这会导致程序偶现闪退,也可能导致数值异常,最终表现为业务逻辑异常,因为回调函数试图访问一个已经失效的栈变量。

修改的方式是,将 st_or_code 变量改为按值捕获。这样,在回调执行时,即使原始的 st_or_code 变量离开了作用域,回调中仍然可以安全地使用其复制的值。下面是修正后的代码:

    std::string WebProxyKeysHelper::GetAuthCode(std::string &st_or_code, std::string &last_key) {...auto ph = (st_or_code == KEY_TYPE_OF_ST_STR ? RefreshProxySt() : RefreshOauthCode());auto prom_ptr = std::make_shared<std::promise<std::string>>();std::future<std::string> fut_pb = prom_ptr->get_future();ph.then([&, st_or_code, prom_ptr](bool ret) { // 注意这里改为按值捕获 st_or_codestd::string tmp_key = "";if (ret) {tmp_key = st_or_code == KEY_TYPE_OF_ST_STR ? proxy_st_ : oauth_code_;UpdateKeys(st_or_code, tmp_key);Schedule();}prom_ptr->set_value(tmp_key);}).onError([&, prom_ptr](const std::exception& ex){prom_ptr->set_value("");});}...return current_key;
}

二、弱引用

2.1 原理

弱引用(Weak Reference)是一种特殊的引用类型,它不会阻止其所引用的对象被垃圾回收。这在处理回调和长时间运行的任务时非常有用,因为它可以避免因为回调导致的潜在内存泄漏。

2.2 案例一

错误的写法:

class Foo {
public:void start() {std::thread t([this]() {std::this_thread::sleep_for(std::chrono::seconds(1));this->doSomething();  // Undefined behavior if `this` is destroyed!});t.detach();}void doSomething() {std::cout << "Doing something..." << std::endl;}
};

在上述代码中,我们在新线程中访问了this指针。然而,如果新线程开始执行时,this指针所指向的对象已经被销毁,这将导致未定义的行为。

正确的写法:

class Foo : public std::enable_shared_from_this<Foo> {
public:void start() {std::thread t([weak_this = std::weak_ptr<Foo>(shared_from_this())]() {if (auto shared_this = weak_this.lock()) {shared_this->doSomething();  // 安全,只要 `this` 没有被销毁}});t.detach();}void doSomething() {std::cout << "Doing something..." << std::endl;}
};

在修正的代码中,我们使用了弱引用来捕获this指针。这样,即使原始对象被销毁,新线程中也不会访问到无效的this指针。

2.3 案例二:使用base库的弱引用

base::BindLambda(base::AsWeakPtr(this), [&] { ... }) 使用了弱引用。这里,base::AsWeakPtr(this)this指针转换为弱引用,并将其传递给Lambda表达式。这样,在回调执行时,如果this指针所指向的对象已经被销毁,回调将不会执行,从而避免了潜在的内存泄漏问题。

下面是执行CGI任务时的回调写法。当CGI网络请求回来时,所在的Service类可能已经被析构,所以需要使用base::AsWeakPtr(this)this指针转换为弱引用:

task->SetCallback(base::BindLambda(base::AsWeakPtr(this), [=](network::ProtocolErrorCode pec, const CRTX_WWK::BatchSetLeaderRsp& resp) {LogicErrorCode code = (pec == network::PEC_OK && task->response_head()->errcode() == 0) ? LEC_OK : LEC_ERROR;if(code == LEC_OK) {...}if (!callback.is_null()) callback.Run(code);
})); 
ScheduleTask(task.get());

大家可能已经注意到,上面的Lamda回调中,我们不需要再额外判断this是否已经被析构,因为base库已经替我们提前判断好再回调:

/*** @brief BindLambda 函数实现了便捷的通过 C++ Lambda 表达式来创建 base::Callback 的方法。* 这个重载允许额外传入一个 base::WeakPtr 类型的弱引用,在实际执行 functor 前会检查弱引用的有效性,如果弱引用已经无效,则不会执行 functor。** @param weakptr 额外传递一个弱引用,在 functor 执行前会进行检查,如果该弱引用无效则不会继续调用 functor* @param functor C++ Lambda 表达式* @param params 需要绑定在 Lambda 表达式上的参数** @note 可根据实际情况,选择使用捕获或者绑定的方式传递参数。*/
template <typename SupportWeakPtrType, typename Functor, typename ...Params>
auto BindLambda(const WeakPtr<SupportWeakPtrType>& weakptr, const Functor& functor, const Params&... params) -> decltype(BindLambda(functor, params...)) {return _WrapWeakCallback(BindLambda(functor, params...), weakptr);
}template <typename SupportWeakPtrType, typename RetType, typename ...Params>
base::Callback<RetType(Params...)> _WrapWeakCallback(const base::Callback<RetType(Params...)>& callback, const WeakPtr<SupportWeakPtrType>& weakptr) {return base::Bind(&_RunWeakCallbackInternalRet<SupportWeakPtrType, RetType, Params...>, weakptr, callback);
}template <typename SupportWeakPtrType, typename RetType, typename ...Params>
RetType _RunWeakCallbackInternalRet(const WeakPtr<SupportWeakPtrType>& weakptr, const base::Callback<RetType(Params...)>& callback, Params... params) {if (weakptr.get()) {return callback.Run(params...);}return RetType();
}
  1. BindLambda 函数接受一个弱引用(weakptr)、一个Lambda表达式(functor)和一些参数(params)。它将创建一个回调函数,该回调在执行前会检查弱引用的有效性。如果弱引用无效,则不会执行Lambda表达式。

  2. _WrapWeakCallback 函数接受一个回调函数(callback)和一个弱引用(weakptr)。它将创建一个新的回调函数,该回调函数在调用之前会检查弱引用的有效性。

  3. _RunWeakCallbackInternalRet 函数在弱引用有效时执行回调函数(callback),否则返回默认值。这个函数实际上是在执行回调之前检查弱引用的有效性的地方。

三、总结

在C++回调中,我们需要根据具体情况选择合适的捕获方式(按值捕获、按引用捕获或弱引用)。在处理回调和长时间运行的任务时,为了避免内存泄漏和访问无效变量的问题,我们通常需要使用按值捕获和弱引用。

最后我们总结一下本文:

类型原理注意事项
按值捕获将外部变量的值复制到Lambda表达式的闭包中,使得Lambda表达式在执行时使用的是复制的值,而不是原始变量的值。如果捕获的变量在Lambda表达式执行时已经离开了作用域,那么按值捕获就是安全的,因为Lambda表达式中使用的是变量的副本。
按引用捕获将外部变量的引用存储在Lambda表达式的闭包中,使得Lambda表达式在执行时直接访问的是原始变量。如果捕获的变量在Lambda表达式执行时已经离开了作用域,那么按引用捕获就可能导致未定义的行为。因此,使用按引用捕获时,需要确保捕获的变量在Lambda表达式执行时仍然有效。
弱引用弱引用是一种特殊的引用类型,它不会阻止其所引用的对象被垃圾回收。这在处理回调和长时间运行的任务时非常有用,因为它可以避免因为回调导致的潜在内存泄漏。如果弱引用所引用的对象在回调执行时已经被销毁,那么回调将不会执行,从而避免了潜在的内存泄漏问题。因此,使用弱引用时,需要确保在回调执行时,弱引用所引用的对象仍然存在。

相关文章:

掌握C++回调:按值捕获、按引用捕获与弱引用

文章目录 一、按引用捕获和按值捕获1.1 原理1.2 案例 二、弱引用2.1 原理2.2 案例一2.3 案例二&#xff1a;使用base库的弱引用 三、总结 在C回调中&#xff0c;当使用Lambda表达式捕获外部变量时&#xff0c;有两种捕获方式&#xff1a;按值捕获和按引用捕获。 一、按引用捕获…...

抖音运营_如何做出优质的短视频

目录 一 短视频内容的构成 1 图像 2 字幕 3 声音 4 特效 5 描述 6 评论 二 短视频的热门类型 1 颜值圈粉类 2 知识教学类 3 幽默搞笑类 4 商品展示类 5 才艺技能类 6 评论解说类 三 热门短视频的特征 1 产生共鸣 2 正能量 3 紧跟热点话题 4 富有创意 四 短视…...

Day21:Leetcode513.找树左下角的值 +112. 路径总和 113.路径总和ii + 106.从中序与后序遍历序列构造二叉树

LeetCode&#xff1a;513.找树左下角的值 解决方案&#xff1a; 1.思路 在遍历一个节点时&#xff0c;需要先把它的非空右子节点放入队列&#xff0c;然后再把它的非空左子节点放入队列&#xff0c;这样才能保证从右到左遍历每一层的节点。广度优先搜索所遍历的最后一个节点…...

Java数据结构和算法(B树)

前言 B树又叫平衡的多路搜索树&#xff1b;平衡的意思是又满足平衡二叉树的一些性质&#xff0c;左树大于右树&#xff1b; 多路意思是&#xff0c;可以多个结点&#xff0c;不再是像二叉树只有两个结点&#xff1b; 实现原理 B树是一种自平衡的搜索树&#xff0c;通常用于实…...

成为程序员后我都明白了什么?从入行到弃坑?

作为一个入行近10年的php程序员&#xff0c;真心感觉一切都才刚开始&#xff0c;对计算机&#xff0c;编程语言的理解也好&#xff0c;程序员中年危机也罢&#xff0c;之前都是听别人说的&#xff0c;真的自己到了这个水平&#xff0c;这个年龄才深刻体会到这其中的种种。 我一…...

python --创建固定字符串长度,先进先出

a 123def concatenate_within_limit(b, new_string):# 计算新字符串与a的长度之和a btotal_length len(a) len(new_string)# 如果长度超过1024&#xff0c;从前面删除足够的字符if total_length > 5:diff total_length - 5a a[diff:] new_string # 删除前diff个字符…...

容器化部署

目录 docker容器化部署 怎样使用Docker Compose或Kubernetes等容器编排工具来管理和扩展联邦学习系统 使用Docker Compose...

国产数据库TiDB的常用方法

TiDB的常用方法主要涉及安装配置、数据操作、性能调优以及监控和维护等方面。以下是对这些常用方法的归纳和介绍&#xff1a; 1. 安装与配置 安装TiDB&#xff1a;根据官方文档的指引&#xff0c;用户可以按照步骤进行TiDB的安装。配置TiDB&#xff1a;安装完成后&#xff0c…...

基于DdddOcr通用验证码离线本地识别SDK搭建个人云打码接口Api

前言 最近介绍了一款免费的验证码识别网站,识别效率太低,考虑到ddddocr是开源的,决定搭建搭建一个,发现原作者sml2h3已经推出好久了,但是网上没有宝塔安装的教程,于是本次通过宝塔搭建属于自己的带带弟弟OCR通用验证码离线本地识别 原项目地址:https://github.com/sml2…...

2、xss-labs之level2

1、打开页面 2、传入xss代码 payload&#xff1a;<script>alert(xss)</script>&#xff0c;发现返回<script>alert(xss)</script> 3、分析原因 打开f12&#xff0c;没什么发现 看后端源码&#xff0c;在这form表单通过get获取keyword的值赋给$str&am…...

人才测评的应用:人才选拔,岗位晋升,面试招聘测评

人才测评自诞生以来&#xff0c;就被广泛应用在各大方面&#xff0c;不仅是我们熟悉的招聘上&#xff0c;还有其他考核和晋升&#xff0c;都会需要用到人才测评。不知道怎么招聘&#xff1f;或者不懂得如何实现人才晋升&#xff1f;都可以参考人才测评&#xff0c;利用它帮我们…...

前端面试题日常练-day33 【面试题】

题目 希望这些选择题能够帮助您进行前端面试的准备&#xff0c;答案在文末。 在jQuery中&#xff0c;以下哪个选项用于在元素上绑定一个点击事件&#xff1f; a) click() b) bind() c) on() d) trigger() jQuery中&#xff0c;以下哪个选项用于获取元素的属性值&#xff1f; …...

非整数倍数据位宽转换24to128

描述 实现数据位宽转换电路&#xff0c;实现24bit数据输入转换为128bit数据输出。其中&#xff0c;先到的数据应置于输出的高bit位。 电路的接口如下图所示。valid_in用来指示数据输入data_in的有效性&#xff0c;valid_out用来指示数据输出data_out的有效性&#xff1b;clk是时…...

html通过数据改变,图片跟着改变

改变前 改变后 通过数据来控制样式展示 <template><div>通过num控制图标是否更改{{num}}<div class"box"><!-- 如果num大于1则是另一种&#xff0c;样式&#xff0c;如果小时1&#xff0c;则是另一种样式 --><div class"item&qu…...

centos7.9 安装SqlServer

1、导入Microsoft SQL Server CentOS存储库&#xff1a; sudo curl -o /etc/yum.repos.d/mssql-server.repo https://packages.microsoft.com/config/rhel/7/mssql-server-2019.repo2、安装SQL Server&#xff1a; sudo yum install -y mssql-server假如机器内存不足2G 需要对…...

Idea中flume的Interceptor的编写教程

1.新建-项目-新建项目 注意位置是将来打包文件存放的位置&#xff0c;即我们打包好的文件在这/export/data个目录下寻找 2. 在maven项目中导入依赖 Pom.xml文件中写入 <dependencies> <dependency> <groupId>org.apache.flume</groupId> <artifa…...

java单元测试:JUnit测试运行器

JUnit测试运行器&#xff08;Test Runner&#xff09;决定了JUnit如何执行测试。JUnit有多个测试运行器&#xff0c;每个运行器都有特定的功能和用途。 1. 默认运行器 当没有显式指定运行器时&#xff0c;JUnit会使用默认运行器&#xff0c;这在JUnit 4和JUnit 5之间有所不同…...

网络模型—BIO、NIO、IO多路复用、信号驱动IO、异步IO

一、用户空间和内核空间 以Linux系统为例&#xff0c;ubuntu和CentOS是Linux的两种比较常见的发行版&#xff0c;任何Linux发行版&#xff0c;其系统内核都是Linux。我们在发行版上操作应用&#xff0c;如Redis、Mysql等其实是无法直接执行访问计算机硬件(如cpu&#xff0c;内存…...

智能语义识别电影机器人的rasa实现

文章目录 0.前言1.项目整体框架2.rasa训练数据结构4.rasa启动命令及用到的API 0.前言 最近做了一个智能电影机器人的项目&#xff0c;我主要负责用户语义意图识别&#xff0c;用的框架是rasa&#xff0c;对应的版本为 3.6.15&#xff0c;对应的安装命令为: pip3 install rasa…...

C# 实现腾讯云 IM 常用 REST API 之会话管理

目录 关于腾讯 IM REST API 开发前准备 范例运行环境 常用会话管理API 查询账号会话总未读数 查询单聊会话消息记录 下载最近会话记录 小结 关于腾讯 IM REST API REST API 是腾讯即时通信 IM 提供给服务端的一组 HTTP 后台管理接口&#xff0c;如消息管理、群组管理…...

[特殊字符] 智能合约中的数据是如何在区块链中保持一致的?

&#x1f9e0; 智能合约中的数据是如何在区块链中保持一致的&#xff1f; 为什么所有区块链节点都能得出相同结果&#xff1f;合约调用这么复杂&#xff0c;状态真能保持一致吗&#xff1f;本篇带你从底层视角理解“状态一致性”的真相。 一、智能合约的数据存储在哪里&#xf…...

日语AI面试高效通关秘籍:专业解读与青柚面试智能助攻

在如今就业市场竞争日益激烈的背景下&#xff0c;越来越多的求职者将目光投向了日本及中日双语岗位。但是&#xff0c;一场日语面试往往让许多人感到步履维艰。你是否也曾因为面试官抛出的“刁钻问题”而心生畏惧&#xff1f;面对生疏的日语交流环境&#xff0c;即便提前恶补了…...

golang循环变量捕获问题​​

在 Go 语言中&#xff0c;当在循环中启动协程&#xff08;goroutine&#xff09;时&#xff0c;如果在协程闭包中直接引用循环变量&#xff0c;可能会遇到一个常见的陷阱 - ​​循环变量捕获问题​​。让我详细解释一下&#xff1a; 问题背景 看这个代码片段&#xff1a; fo…...

【第二十一章 SDIO接口(SDIO)】

第二十一章 SDIO接口 目录 第二十一章 SDIO接口(SDIO) 1 SDIO 主要功能 2 SDIO 总线拓扑 3 SDIO 功能描述 3.1 SDIO 适配器 3.2 SDIOAHB 接口 4 卡功能描述 4.1 卡识别模式 4.2 卡复位 4.3 操作电压范围确认 4.4 卡识别过程 4.5 写数据块 4.6 读数据块 4.7 数据流…...

【论文笔记】若干矿井粉尘检测算法概述

总的来说&#xff0c;传统机器学习、传统机器学习与深度学习的结合、LSTM等算法所需要的数据集来源于矿井传感器测量的粉尘浓度&#xff0c;通过建立回归模型来预测未来矿井的粉尘浓度。传统机器学习算法性能易受数据中极端值的影响。YOLO等计算机视觉算法所需要的数据集来源于…...

让AI看见世界:MCP协议与服务器的工作原理

让AI看见世界&#xff1a;MCP协议与服务器的工作原理 MCP&#xff08;Model Context Protocol&#xff09;是一种创新的通信协议&#xff0c;旨在让大型语言模型能够安全、高效地与外部资源进行交互。在AI技术快速发展的今天&#xff0c;MCP正成为连接AI与现实世界的重要桥梁。…...

Qt 事件处理中 return 的深入解析

Qt 事件处理中 return 的深入解析 在 Qt 事件处理中&#xff0c;return 语句的使用是另一个关键概念&#xff0c;它与 event->accept()/event->ignore() 密切相关但作用不同。让我们详细分析一下它们之间的关系和工作原理。 核心区别&#xff1a;不同层级的事件处理 方…...

DeepSeek源码深度解析 × 华为仓颉语言编程精粹——从MoE架构到全场景开发生态

前言 在人工智能技术飞速发展的今天&#xff0c;深度学习与大模型技术已成为推动行业变革的核心驱动力&#xff0c;而高效、灵活的开发工具与编程语言则为技术创新提供了重要支撑。本书以两大前沿技术领域为核心&#xff0c;系统性地呈现了两部深度技术著作的精华&#xff1a;…...

Java详解LeetCode 热题 100(26):LeetCode 142. 环形链表 II(Linked List Cycle II)详解

文章目录 1. 题目描述1.1 链表节点定义 2. 理解题目2.1 问题可视化2.2 核心挑战 3. 解法一&#xff1a;HashSet 标记访问法3.1 算法思路3.2 Java代码实现3.3 详细执行过程演示3.4 执行结果示例3.5 复杂度分析3.6 优缺点分析 4. 解法二&#xff1a;Floyd 快慢指针法&#xff08;…...

js 设置3秒后执行

如何在JavaScript中延迟3秒执行操作 在JavaScript中&#xff0c;要设置一个操作在指定延迟后&#xff08;例如3秒&#xff09;执行&#xff0c;可以使用 setTimeout 函数。setTimeout 是JavaScript的核心计时器方法&#xff0c;它接受两个参数&#xff1a; 要执行的函数&…...