C++ 的衰退复制(decay-copy)
目录
1.什么是衰退复制(decay-copy)
1.1.推导规则
1.2.LWG issue 929
1.3.想象中的 decay_copy
2.decay-copy 与 auto
2.1.为什么引入衰退复制
2.2. 成为 C++ 23 的语言特性
3.应用场景
4.总结
1.什么是衰退复制(decay-copy)
1.1.推导规则
要理解为什么 C++ 会有衰退复制(decay-copy)这个需求,需要了解一下 LWG issue 929 问题的提出,但是要理解这个 issue,最好先复习一下 C++ 模板参数的推导规则。我们按照模板的形参类型进行以下分类,大致可分为三类(不讨论 auto 的推导):
形参类型 | 基本规则 |
非转发引用类型的引用 : template<typename T> void f(T& param); | f(expr); 忽略实参表达式中的引用,将实参表达式 expr 与 param 做模式匹配,确定 T 的类型 |
转发引用: template<typename T> void f(T&& param); | f(expr); 如果 expr 是类型 E 的左值,则 T 被推导为 E&,经过引用折叠后将 param 的类型生成为 E&。对于其他情况,按照普通引用的规则推导(前面一条)。 |
传值参数: template<typename T> void f(T param); | f(expr); 如果 expr 的类型是引用类型,则忽略引用; 如果 expr 表达式含有 CV 限定,则忽略 CV 限定; 然后与 param 做模式匹配,确定 T 的类型。 重点:类型退化 |
我们不讨论 auto 的推导规则,也不考虑形参表达式与实参表达式的模式匹配的细节,上表是类型推导的基本规则,其中重点是传值参数的类型退化。退化主要有两点,就是数组类型会在推导前退化成指针,函数类型也会在推导前退化成函数指针。当形参类型是引用类型的时候,不会发生退化,这就是为什么如果我们希望推导类型是数组类型,就只能使用引用类型的原因。
1.2.LWG issue 929
在制定 C++ 11 标准的过程中,C++ 标准委员会的标准库工作组(Library Working Group)收到一个 Issue,就是这个:LWG issue 929 [资料 1]。C++ 11 新增了对并发(concurrency support library)的支持,但是在使用普通函数构造线程的时候会遇到一些麻烦,比如这段代码:
void f(const char*);
std::thread t(f,"hello");
std::thread 的构造函数使用右值引用传递可调用物和可调用物的参数,并且要求可调用物对应的参数支持可复制构造(CopyConstructible)特性。这样的约束过于严格,限制是使用普通函数作为可调用物的可能性。因为在 std::thread 的构造函数(函数模板)参数推导过程中,因为参数类型使用了右值引用,则绑定的参数类型会被推导为函数的引用类型,而不是退化为函数指针。 于此同时,对参数类型使用右值引用,也阻止了从数组到指针的退化。由于数组不可复制,不可移动,所以结果就是阻止了数组作为可调用物的参数使用的可能性。在这个例子中,字符串字面量会被推导为 const char[6] 类型,显然与 const char * 是不匹配的,这就也阻止了在这种情况下使用字符串字面量的可能。
如果将 std::thread 的构造函数的参数改成传值类型,数组可以退化成指针,就消除了要求数组具备可复制构造的矛盾。但是传值参数会引起参数传递的效率问题,可见问题没有这么简单。
1.3.想象中的 decay_copy
既然不能简单使用传值参数,那么可不可以在参数推导的时候做点手动退化呢?当然可以,LWG issue 929 建议引入一个类似这样的 decay_copy() 函数:
template<typename T>
typename decay<T>::type decay_copy(T&& v);
以便得到一个 v 的右值副本,用于右值引用类型的传递。有了 decay_copy() 函数,你可以将标准库中 std::thread 的构造函数大致理解为这样的实现:
template <class Function, class Arg>
thread(Function&& f, Arg&& arg) {std::invoke(std::decay_copy(std::forward<Function>(f)),std::decay_copy(std::forward<Arg>(arg)));
}
decay_copy() 函数可以将参数拷贝或移动到参数类型的退化版本(的副本)上,与此同时带来的后果就是,当你真的需要传递引用的时候,你需要用 C++ 的引用包装器std::ref 和 std::cref来对抗这种退化。
好吧,必需得承认,上面的 decay_copy() 函数是我们的想象,C++ 只是引入了衰退复制的概念,但是并没有在库中添加这个函数。至于为什么引入这个概念,以及最后到底怎么实现了这个概念,请继续看下去。
2.decay-copy 与 auto
2.1.为什么引入衰退复制
C++ 11 引入衰退复制的目的仅仅是为了满足并发库的特殊兴趣爱好吗?当然不是,衰退复制其实是一种具体使用场景的需求,一句话描述这个场景就是:当你需要传递一个对象的副本(拷贝)给一个操作,但是这个操作只接受对象的引用。在这种情况下,如果我们传递的是左值类型的副本,就必需小心维护这个副本的生命周期,否则很可能会发生引用悬挂,如果传递的是右值副本,则可以通过绑定到右值引用而延长生命周期,避免引用悬挂,会相对比较安全。
根据第 1 节的介绍,我们其实已经知道,所谓的衰退复制,其实就是得到对象的一个纯右值副本。LWG issue 929 建议增加一个 decay_copy() 函数,但是并没有被 C++ 标准采纳。以下是很多资料中都提到的这个函数的具体实现:
template<class T>
constexpr std::decay_t<T> decay_copy(T&& v) noexcept(std::is_nothrow_convertible_v<T, std::decay_t<T>>) {return std::forward<T>(v);
}
很多情况下,我们可以显式使用 decay_copy() 函数,达到我们的目的。比如这个例子:
void ProcessFoo(Foo&& f);Foo foo{ 42 };ProcessFoo(foo); //错误,foo 是左值
ProcessFoo(std::move(foo)); // 可以,但是慎重
ProcessFoo(decay_copy(foo)); //OK,foo 还活着
在这个例子中,使用 move 可以满足函数调用的要求,但是要慎重,在某些场合, foo 可能会真的被移走,这可能不是我们希望的结果。使用 decay_copy() 就放心多了,通过函数返回值得到一个纯右值副本,也不需要关心它的生命周期维护问题。
衰退复制的思想在线程库、ranges 库大量使用,在一些异步调用的过程中,以及变量需要被延迟绑定的场合,也会需要这个概念。
2.2. 成为 C++ 23 的语言特性
为什么标准库不要 decay_copy() 函数?上面给了这样一个例子:
class A {int x;public:A();auto run() {f(A(*this)); // okf(auto(*this)); // ok as proposedf(decay_copy(*this)); // ill-formed}protected:A(const A&);
};
要拿到一个对象的副本,需要能直接访问对象的拷贝构造函数或移动构造函数,对于这个例子,因为 decay_copy() 函数不是 A 的友元函数,无法访问 A 的构造函数,这就限制了 decay_copy() 函数的使用,毕竟,标准库也不可能把这个函数声明成任何对象的友元。
从 C++ 11 开始,auto 关键字就一直是个多才多艺的家伙,从自动推导类型占位符到 C++ 20 的模板化 lambda 表达式,auto 关键字的出镜率极高。尽管如此,还有两个表达形式没有被用到,那就是独立的 auto(x) 和 auto{x} 表达式。既然闲着,就拉来加个班吧,于是,用 auto 做纯右值副本,成了 C++ 23 的语言特性。用 auto(x) 或 auto{x} 代替 decay_copy() 函数,不仅仅是查找替换这么简单,auto 有更好的性能。 decay_copy() 函数无论如何都会产生一个对象副本,但是 auto(x) 更智能一点,如果 x 本身就是一个纯右值的话,auto(x) 不做任何操作,不产生开销。对于上一节的 ProcessFoo() 函数,用 auto(x) 代替 decay_copy() 函数:
ProcessFoo(auto(foo)); // OK
ProcessFoo(auto{foo}); // OK
可能有人会想,已知对象类型是 T ,直接 T() 或 T{} 构造一个对象副本不行吗?为什么还要增加这个语言特性?首先直接构造对象副本存在与 decay_copy() 函数一样的问题,那就是要解决拷贝构造函数或移动构造函数的可访问性,还有就是无论如何都产生副本的操作,在 x 本身是右值的情况下有点多余,这是性能问题。其次是在一些泛型编程或模板库中,T 的具体类型不是那么具体,比如这个例子:
void pop_front_alike(Container auto& x) {std::erase(x.begin(), x.end(), auto(x.front()));
}
auto(x.front()) 可以轻松得到一个 x.front() 的右值副本,如果尝试直接调用构造函数,就会麻烦很多:
void pop_front_alike(Container auto& x) {using T = std::decay_t<decltype(x.front())>;std::erase(x.begin(), x.end(), T(x.front()));
}
首先要用 decltype 得到 x.front() 返回值的类型,然后还要手动退化一下,因为得到的类型可能是引用类型,或者有 CV 限定。这么做不如 auto 语言特性来的直接,并且如果容器 Container 的 front() 返回类型本身就是右值,这里用 auto 就不会产生任何开销。
3.应用场景
1) 模板参数推导
在模板编程中,当模板函数的参数类型与传入的实参类型不匹配时,编译器会根据一系列推导规则来确定模板参数的具体类型。这个过程中可能会发生衰退复制,特别是当实参是数组或函数类型时,它们可能会被推导为相应的指针类型。
C++之std::decay
2) 并发编程中的线程构造
在C++11及以后的标准中,引入了并发编程的支持,包括线程(std::thread)等。当使用std::thread构造函数创建线程时,需要传递一个可调用对象(如函数、函数对象、lambda表达式等)及其参数。如果传递的参数类型与std::thread构造函数期望的参数类型不匹配,则可能会发生衰退复制。
std::thread使用及实现原理精讲(全)-CSDN博客
4.总结
C++中的衰退复制是一个与类型推导和参数传递相关的复杂过程。在编写C++代码时,需要仔细考虑类型匹配和参数传递方式,以避免不必要的衰退复制和潜在的类型错误。
从 C++ 23 开始,标准库中原来使用的类似 decay_copy() 函数那样的表达形式,都被 auto(x) 取代,在某些情况下得到了性能提升。
相关文章:
C++ 的衰退复制(decay-copy)
目录 1.什么是衰退复制(decay-copy) 1.1.推导规则 1.2.LWG issue 929 1.3.想象中的 decay_copy 2.decay-copy 与 auto 2.1.为什么引入衰退复制 2.2. 成为 C 23 的语言特性 3.应用场景 4.总结 1.什么是衰退复制(decay-copy࿰…...
vue-cli 5接入模块联邦 module federation
vue-cli 5接入模块联邦 module federation 模块联邦概念实现思路配置遇到的问题: 模块联邦概念 模块联邦由webpack 5最先推出的,让应用加载远程的代码模块来实现不同的Web应用共享代码片段.模块联邦分为两个角色,一个是生产者,一个是消费者.生产者暴露代码供消费者消费 (用一个…...
【Rust自学】3.6. 控制流:循环
3.6.0. 写在正文之前 欢迎来到Rust自学的第三章,一共有6个小节,分别是: 变量与可变性数据类型:标量类型数据类型:复合类型函数和注释控制流:if else控制流:循环(本文) 通过第二章…...

【第八节】git与github
目录 前言 一、 远程仓库概述 二、 创建、配置、连接推送远程仓库 2.1 在 GitHub 上创建仓库 2.2 生成 SSH Key 2.3 验证 SSH 连接 2.4 本地初始化仓库 2.5 推送本地仓库到远程 三、 管理远程仓库 3.1 查看远程仓库 3.2 提取远程仓库更新 3.3 推送更新到远程仓库 …...

win如何访问Linux数据库(本地)
对于数据库的学习,我们都是在localhost主机上进行操作,当我们在Linux系统上安装数据库时,我们就有了尝试在win上去访问Linux上的数据库的想法。 数据库中的用户: 我们都知道数据库中顶级的用户为root,在做创建用户的联…...

Windows设置所有软件默认以管理员身份运行
方法一、修改注册表 winr打开运行,输入“regedit”打开注册表; 打开此路径“计算机HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindowsCurrentVersionPoliciesSystem”; 在右侧找到“EnableLUA”,将其值改为0,重启电脑。 …...

前端 计算发布时间(如“1小时前”、“3天前”等)
这样效果,在c端比较常见,通过前端也可以处理 代码如下: // 计算 小时timeAgo(createTime) {// 将 createTime 字符串转换为 Date 对象 const createDate new Date(createTime);const now new Date();const diffInSeconds Math.floor((now…...

shardingjdbc 4.0.0 seata分布式事务Failed to fetch schema问题
报错 12-18 15:18:35.931 [ERROR] [i.s.r.d.s.s.cache.AbstractTableMetaCache:63 ] [traceId:][spanId:] - get table meta of the table wh_stock_log error: Failed to fetch schema of xxx java.sql.SQLException: Failed to fetch schema of wh_stock_logat io.seata.r…...

罗德与施瓦茨NRT2功率反射仪,NRT2通过式功率计
罗德与施瓦茨NRT2功率反射仪NRT2 通过式功率计 描述 定向/通过式功率传感器在线测量正向和反向功率。在安装、维修和监控发射机、天线和射频发生器时,需要进行这些测量。R&SNRT系列由R&SNRT2功率反射计及各种R&SNRT Zxx定向功率传感器。 由于其测量功…...
QLineEdit限制输入固定字节数(UTF-8编码)
setMaxLength(int)只能用来限制输入的字符个数 QLineEdit *editor new QLineEdit(parent); editor->setMaxLength(32); 1、如果是单字节字符,如数字,字母等,字符数正好等于字节数 2、如果是多字节字符,UTF8编码时࿰…...

基于ubuntu的mysql 8.0安装教程
文章目录 1.查看版本2.切换到root账户3.下载安装包4.问题的解决5.查看是否解压成功6.安装我们的发布包7.更新包的内容8.下载mysql9.查看mysql的状态10.设置开机自启动11.登录mysql 公司里面的mysql根本不会出现在windows操作系统上面,下面我们演示的就是如何在ubunt…...
K8s ConfigMap的基础功能介绍
在 Kubernetes 中,ConfigMap 是一种用于管理配置信息的资源对象,它允许你将 配置信息与代码解耦,方便管理和更新应用配置,而无需重新构建镜像或重启服务。 ConfigMap 的功能 存储配置信息: 可以以 键值对 的形式存储配…...

Linux——Shell
if 语句 格式:if list; then list; [ elif list; then list; ] ... [ else list; ] fi 单分支 if 条件表达式; then 命令 fi 示例: #!/bin/bash N10 if [ $N -gt 5 ]; then echo yes fi # bash test.sh yes 双分支 if 条件表达式; then 命令 else 命令…...

armsom产品编译烧录Linux固件
1、开发环境及工具准备 Rockchip Linux 软件包:linux-5.10-gen-rkr4 主机: 安装VMware搭建虚拟机,版本为Ubuntu 20.04 (硬盘容量大于100G) 安装远程连接工具MobaXterm(可连接虚拟机方便文件传输) 2、S…...

VSCode:Markdown插件安装使用 -- 最简洁的VSCode中Markdown插件安装使用
VSCode:Markdown插件安装使用 1.安装Marktext2.使用Marktext 本文,将在Visual Studio Code中,安装和使用Markdown插件,以Marktext插件为例。 1.安装Marktext 打开VSCode,侧边栏中找到扩展模块(或CtrlShiftX快捷键)&am…...
AI 行业发展趋势:科技创新引领未来变革
在当今数字化时代,人工智能(AI)行业正以前所未有的速度蓬勃发展,深刻地改变着我们的生活、工作和社会格局。从基础技术的突破到广泛的应用场景拓展,AI 展现出了一系列令人瞩目的发展趋势,预示着一个充满无限可能的未来。 一、技术创新持续突破 模型规模与性能提升 AI 模…...

FB爆款打法实操经验总结
在进行Facebook广告投放时,有效的预算控制、素材测试、广告效果评估和lookalike受众的管理是至关重要的。通过科学的方法和策略,您可以在竞争激烈的市场中实现更好的业绩。 01 预算控制 测试阶段的广告不稳定性:在投放广告的初期,…...

微信小程序TTS解决方案
微信小程序原生语音合成 API(基础且简单) 介绍:微信小程序提供了基础的语音合成能力。通过wx.createInnerAudioContext()等相关API,可以实现简单的语音播放功能。不过它主要是用于音频播放,对于完整的文本到语音&#…...

centos stream 8下载安装遇到的坑
早在2020年12月。CentOS 官方发文宣称:“CentOS项目的未来是 CentOS Stream 明年我们会将重点从CentOS Linux 转移到CentOS Stream 它紧随当前 RHEL 版本之前。CentOS Linux 8 作为 RHEL 8 的重建,将于 2021 年底结束。CentOS Stream 在该日期之后继续&a…...
计算机网络——期末复习(1)背诵
背诵 交换机与路由器:交换机连接同一子网,利用帧中的目的物理地址转发帧,工作在数据链路层;路由器连接不同子网,利用IP数据报中的目的IP地址转发IP数据报,工作在网络层。五层的任务:࿰…...

【配置 YOLOX 用于按目录分类的图片数据集】
现在的图标点选越来越多,如何一步解决,采用 YOLOX 目标检测模式则可以轻松解决 要在 YOLOX 中使用按目录分类的图片数据集(每个目录代表一个类别,目录下是该类别的所有图片),你需要进行以下配置步骤&#x…...

2025盘古石杯决赛【手机取证】
前言 第三届盘古石杯国际电子数据取证大赛决赛 最后一题没有解出来,实在找不到,希望有大佬教一下我。 还有就会议时间,我感觉不是图片时间,因为在电脑看到是其他时间用老会议系统开的会。 手机取证 1、分析鸿蒙手机检材&#x…...
C# SqlSugar:依赖注入与仓储模式实践
C# SqlSugar:依赖注入与仓储模式实践 在 C# 的应用开发中,数据库操作是必不可少的环节。为了让数据访问层更加简洁、高效且易于维护,许多开发者会选择成熟的 ORM(对象关系映射)框架,SqlSugar 就是其中备受…...

全志A40i android7.1 调试信息打印串口由uart0改为uart3
一,概述 1. 目的 将调试信息打印串口由uart0改为uart3。 2. 版本信息 Uboot版本:2014.07; Kernel版本:Linux-3.10; 二,Uboot 1. sys_config.fex改动 使能uart3(TX:PH00 RX:PH01),并让boo…...
Android第十三次面试总结(四大 组件基础)
Activity生命周期和四大启动模式详解 一、Activity 生命周期 Activity 的生命周期由一系列回调方法组成,用于管理其创建、可见性、焦点和销毁过程。以下是核心方法及其调用时机: onCreate() 调用时机:Activity 首次创建时调用。…...

永磁同步电机无速度算法--基于卡尔曼滤波器的滑模观测器
一、原理介绍 传统滑模观测器采用如下结构: 传统SMO中LPF会带来相位延迟和幅值衰减,并且需要额外的相位补偿。 采用扩展卡尔曼滤波器代替常用低通滤波器(LPF),可以去除高次谐波,并且不用相位补偿就可以获得一个误差较小的转子位…...

java高级——高阶函数、如何定义一个函数式接口类似stream流的filter
java高级——高阶函数、stream流 前情提要文章介绍一、函数伊始1.1 合格的函数1.2 有形的函数2. 函数对象2.1 函数对象——行为参数化2.2 函数对象——延迟执行 二、 函数编程语法1. 函数对象表现形式1.1 Lambda表达式1.2 方法引用(Math::max) 2 函数接口…...

链式法则中 复合函数的推导路径 多变量“信息传递路径”
非常好,我们将之前关于偏导数链式法则中不能“约掉”偏导符号的问题,统一使用 二重复合函数: z f ( u ( x , y ) , v ( x , y ) ) \boxed{z f(u(x,y),\ v(x,y))} zf(u(x,y), v(x,y)) 来全面说明。我们会展示其全微分形式(偏导…...
拟合问题处理
在机器学习中,核心任务通常围绕模型训练和性能提升展开,但你提到的 “优化训练数据解决过拟合” 和 “提升泛化性能解决欠拟合” 需要结合更准确的概念进行梳理。以下是对机器学习核心任务的系统复习和修正: 一、机器学习的核心任务框架 机…...
C#最佳实践:为何优先使用as或is而非强制转换
C#最佳实践:为何优先使用as或is而非强制转换 在 C# 的编程世界里,类型转换是我们经常会遇到的操作。就像在现实生活中,我们可能需要把不同形状的物品重新整理归类一样,在代码里,我们也常常需要将一个数据类型转换为另…...