Modern C++ | 谈谈万能引用以及它的衍生问题:将亡值、引用折叠和完美转发

news2024/11/26 10:37:23

文章目录

    • 前言
    • 左右值引用的铺垫
    • 万能引用&&引用折叠
    • 完美转发

前言

在学习Linux系统编程的过程中,想着得到了新知识,不能把旧知识忘了啊,所以我就读起了以前写的博客,在Modern C++介绍这篇博客中,关于完美转发只是介绍了其用法,感觉差了点什么,于是就是去看了看别人对于完美转发的理解。结果发现这玩意有些复杂,索性以我的知识理解再写一篇博客(网上资料的质量参差不齐,想查清楚一个语法点真的痛苦)。

左右值引用的铺垫

(在讲解万能引用之前,先简单的聊一聊左值引用和右值引用)

在关于引用的理解这篇文章中我说,引用其实是一层软件层,将使用者与语言的底层结构解耦,其实C++的设计者是想让我们多使用引用而少使用指针的,想让我们通过变量名或者引用访问底层的地址,修改地址上存储的数据,而不是直接通过地址访问地址上的数据。具体的理解可以看我的上一篇文章。既然引用只是索引底层地址的一种节点,表示变量名与地址之间的映射,那么就可以有很多变量名映射同一个地址,一个变量可以有很多的引用,但是引用之间可以建立映射关系吗?或者说存在引用的引用这样的类型吗?答案是不存在的,当引用与一个变量建立映射关系,本质是与变量的地址建立映射关系,我们要看到变量后面的地址。所以引用其实和普通变量一样,都指向了底层的相同地址,那什么叫做引用的引用,它们不都指向了底层的地址吗?

int x = 0;
int& y = x;
int& z = y;

上面的demo中,z是y的引用,y又是x的引用,很显然,z是一个引用的引用,但是z和x一样,都指向了x的地址,而不是y的地址,y又没有地址,y只是与x的地址建立了映射关系,只是一个访问x的窗口,并不是一个实体。如果你深刻理解引用的概念,就知道引用的引用是一个不存在的概念,引用的引用不还是和引用一样,指向了底层的地址吗,所以我们不用像指针一样搞那么复杂,理解什么指针的指针,只要记住C++中只有引用。
在这里插入图片描述
但是左值引用和右值引用却有着根本上的区别,左值引用是去引用一个已经存储在可写数据区中的变量,而右值引用是去引用存储在只读数据区的变量或数据,但是我们需要通过引用修改其引用地址上的数据,被引用的对象在只读数据区中要怎么修改?当然是不能修改的,所以程序这时会在可写数据区中开辟一块空间,把只读数据区的对象拷贝到可写数据区中。通过上图的代码测试,可以看到右值引用的地址和普通的左值变量地址紧挨着,由此我们可以推断,当引用一个右值时,该右值会被拷贝到可写数据区中,引用的右值变成了一个左值,或者说引用右值可以等价于普通变量的开辟+左值引用的创建,具体可以看我的上篇博客

其实右值引用并不是这样使用的,我们不应该引用这些存储在可读数据区的变量,正常情况下我们也没有引用这些右值的需求。我们应该引用将亡值,什么意思呢,虽然编译器不允许我们直接访问将亡值,因为将亡值的生命周期马上要结束了,资源将要被释放了,我们不能也没有必要去获取一个将亡值。所以编译器就对将亡值进行了限制,在语法层面上对将亡值的访问进行了限制。比如int& x = 1.1;这行表达式产生的中间变量我们无法获取,但是将亡值的存储区域是可写数据区,理论上我们是可以访问将亡值的,因此C++也为我们开了一道口子,我们可以使用右值引用获取一个将亡值,此时将亡值的生命周期并没有延长,出了作用域将亡值就被释放,我们不能再通过将亡值使用它的资源,但是它的资源将被我们自己的左值继承下来,可以说我们延长了将亡值所拥有资源的生命周期。所以,为啥要叫右值引用,叫它将亡值引用不是更贴切吗?

上篇博客的最后,我说右值引用得到的引用不能作为实参调用形参为右值引用的函数,这是无法实现的,因为右值引用得到的引用是一个具名对象,我们通过引用可以访问引用对象上的数据,那么被引用对象就是一个左值,很显然,这时的右值引用对象,引用的不是一个右值而是一个左值。那么我们要怎么调用形参为右值引用的函数呢?一是直接将字面常量作为函数的实参,用纯右值调用形参为右值引用的函数,这时函数的形参会拷贝一份纯右值到可写数据区中,引用拷贝的对象,这个形参就又变为了左值。除此之外,将亡值也可以调用形参为右值引用的函数,这也是移动构造和移动赋值的实现原理,那么现在的问题就是要怎么得到将亡值?我们知道将亡值是一个右值,如果你要右值引用,引用一个将亡值,那么得到的对象其实就是一个左值了,无法调用形参为右值引用的函数,我们只能通过创建匿名对象的表达式以及一些返回右值引用的函数得到将亡值,从而调用形参为右值引用的函数,我们知道函数的返回值在没有被接收之前一直是匿名的,是一个右值,也是一个将亡值。由于这篇博客不讨论移动构造和移动赋值,这里我们不再深入

万能引用&&引用折叠

使用模板参数时,为模板参数加上右值引用的符号(具体见下面的代码),这样的模板参数可不是只能用来接收右值引用的,它还可以接收左值引用,我们称它为万能引用

template <class T>
void test(T&& x);

那么为什么会T&&就是万能引用,T&就不是万能引用?这就要涉及到引用的引用和模板参数的推导了。我们知道,调用函数却不显式的指定模板参数时,编译器会自动根据实参类型推导模板参数,比如

template <class T>
void swap(T left, T right)

int main()
{
	int num1 = 10;
	int num2 = 20;
	swap(num1, num2);
	return 0;
}

上面的demo中,由于num1和num2的类型时int,编译器自动推导生成的模板函数就是void swap(int left, int right),可以看到T被推导为实参的类型int了。那么实参的类型是int&或者int&&呢?T就被推导为int&与int&&,假如T也有引用呢?比如一开始举的例子中的test函数,void test(T&& x)

形参的类型为T&&
当实参类型为int&,T被推导为T&,形参就被推导为int& &&
当实参类型为int&&,T被推导为T&&,形参就被推导为int&& &&

这里要注意T的类型和形参的类型,一开始我们就说:没有引用的引用这样的概念,很显然函数的形参被推导成为了引用的引用,编译器会怎样看待引用的引用?虽然我们不能显式的写出引用的引用,但是在实例化模板参数时却会出现引用的引用,编译器将根据

两个引用中只要有一个左值引用,最终的引用类型就是左值引用
如果都是右值引用,最终的引用类型就是右值引用

这样的规则推导最终的引用类型,比如int& &&,因为其中有一个左值引用,所以它最终的类型就是int&。int&& &&,因为两个引用都是右值引用,所以它最终的类型就是int&&。这就是引用折叠,只要出现引用的引用,编译器就会推导最终的引用类型,不可能让我们继续套娃下去。所以啊,根据这个规则T&&接收左值引用,最终推导的引用也是左值引用,接收右值引用,最终推导的引用也还是右值引用,但是T&不论接收左值还是右值的引用,最终的引用都是左值引用,也就是说只有T&&即可以接收左值还可以接收右值,所以将其称之为万能引用

有了引用折叠的理论知识,我们再来看一个例子

template <class T>
void test(T&& x)
{
	print(forward<T>(x));
}
int main()
{
	int x = 0;
	int& y = x;
	int&& z = 1;

	test<int&&>(z);
	return 0;
}

我们知道虽然z是右值引用,但其依然是一个左值,调用test函数时,我们传入模板参数int&&,T被实例化为int&&,根据引用折叠:int&& && --> int&&,x的类型是int&&,是一个右值引用,只能接收右值或者将亡值,z作为一个左值,显然不能调用这样实例化的test函数
在这里插入图片描述
修改为模板参数传递的实参,将其修改为int&,T被实例化为int&,根据引用折叠:int& && --> int&,最终x的类型是一个左值引用,可以接收左值和左值引用,所以此时编译通过
在这里插入图片描述

完美转发

当一个右值传递给一个函数后,函数肯定是用右值引用接收右值的,所以这个右值就失去了右值属性,如果现在想用这个右值移动构造一个对象呢?显然是做不到的,因为它现在是一个左值,无法调用移动构造函数,但是这个左值原来是一个右值啊,有没有什么方法可以恢复它原来的右值属性并传递给其他函数呢?答案是有的,我们可以通过一个函数,这个函数将返回右值引用,此时的右值引用就是实实在在的右值了,先看代码


void print(int& x)
{
	printf("void print(int& x)\n");
}

void print(int&& x)
{
	printf("void print(int&& x)\n");
}

template <class T>
void test(T&& x)
{
	print(x);
	print(forward<T>(x));
}

int main()
{
	int x = 0;
	int& y = x;
	int&& z = 1;

	test(1);
	return 0;
}

test函数将右值,1作为函数的实参,形参x接收右值1后,成为了左值,这个时候再调用两次print函数,一次是直接将x作为实参调用,一次是将x完美转发后调用在这里插入图片描述
通过结果可以看到,只有完美转发后的x调用了右值引用版本的print函数,没有转发的x是一个左值,调用左值版本的print函数。完美转发函数forward是怎么是实现的?先来看函数原型

template <class _Ty>
_NODISCARD constexpr _Ty&& forward(
    remove_reference_t<_Ty>& _Arg) noexcept { // forward an lvalue as either an lvalue or an rvalue
    return static_cast<_Ty&&>(_Arg);
}

remove_reference_t<_Ty>的意思是移除参数_Ty的引用属性,如果_Ty是int&&,移除后就是int,我们看到forward函数的返回值是一个右值引用_Ty&&,作为函数返回值,此时的右值引用是可以被形参为右值引用的函数接收的。forward函数的形参是一个左值引用remove_reference_t<_Ty>&,将_Ty的引用属性去除后,再加上了左值属性,也就是说forward函数可以接收实参类型是左值和左值引用的数据,1被test函数接收后,成为了一个左值,并且test函数的模板参数T被推导为int,此时调用forward,test将模板参数T给forward,forward的形参为int&,可以接收变为左值的x。forward函数返回一个右值引用,static_cast<_Ty&&>(_Arg)将形参_Arg强制类型转换为_Ty&&,_Ty是int,最终_Arg被转换为int&&,且作为函数返回值返回。然后再作为print的实参调用print,此时调用的就是右值版本的print。

如果转发的变量是一个左值呢?将一个左值传给test函数,模板参数T推导的结果也是int啊,和右值一样,那么test的形参T&&作为万能引用是怎么引用左值的呢?其实这里比较特殊,T&&最终会是一个引用,当T被推导成右值,比如说int,T&&就是int&&,右值传给右值引用,没有问题。当T被推导成左值呢?如果还是int,最终的int&&不就成为了一个右值引用吗?所以当左值作为万能引用的模板参数时,万能引用会被推导为左值引用,比如说一个int类型的左值,int i = 1; test(i); 这里因为i是左值,T就会被推导为int&,也就是说万能引用把左值和左值引用看出同一个类型了,其实这也是正确的,左值引用与普通的左值不都是通过变量名索引地址吗?从底层的角度讲是没有什么区别的,并且这些做也能解决万能引用的左值问题。

所以当一个左值作为万能引用的模板参数时,编译器会把它当成左值引用


void print(int& x)
{
	printf("void print(int& x)\n");
}

void print(int&& x)
{
	printf("void print(int&& x)\n");
}

template <class T>
void test(T&& x)
{
	print(x);
	print(forward<T>(x));
}

int main()
{
	int x = 0;
	int& y = x;
	int&& z = 1;

	test(x);
	return 0;
}

比如将x作为test的实参,test的模板参数T是一个万能引用,将左值int推导为int&,等价于左值引用,然后调用forward时,用int&将模板参数实例化。return static_cast<_Ty&&>(_Arg),再看forward函数的返回值,_Ty被显式实例化为int&,所以_Ty&& --> int& && --> int&,左值最终被转发成左值引用,与原来的值类别一样。而左值引用和右值引用的情况也是差不多的,只要注意引用折叠,就能理解完美转发是怎样转发对象的值类别的

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

安科瑞智能仪表在密集母线行业中的应用

安科瑞 华楠AMB100智能母线监控系列系统示意图功能1.电压、电流、频率、有功功率、无功功率、功率因数、有功电能、无功电能、2-63次谐波、温度、湿度、漏电流等电参量测量&#xff1b;2.交流2DI、2DO&#xff0c;直流4DI、2DO&#xff1b;3.可信号取电或者独立辅助电源供电&am…

windows安装VMware最新版本(VMware Workstation 17.0 Pro)详细教程

目录 一、概述 二、下载 VMware Workstation 17.0 Pro 三、安装 VMware Workstation 17.0 Pro 四、创建一个空的虚拟机 一、概述 VMware Workstation Pro™ 是一个运行在window或Linux系统的软件&#xff0c;使开发人员能够在同一台 PC 上同时运行多个基于 x86 的 Windows、Li…

国内最全的Spring Boot系列之六

在新的一年祝大家兔年大吉&#xff0c;兔耳冲天&#xff0c;动如脱兔! 2022年就这么过去了&#xff0c;闭上眼回首2022年发生的事情&#xff0c;犹如过眼云烟 —— 一事无成的感觉。 2022年到底都发生了什么事情&#xff0c;坚持了什么&#xff1f;于是我闭上眼睛&#xff0c…

2023-1-16 刷题情况

子相似性 III 题目描述 一个句子是由一些单词与它们之间的单个空格组成&#xff0c;且句子的开头和结尾没有多余空格。比方说&#xff0c;“Hello World” &#xff0c;“HELLO” &#xff0c;“hello world hello world” 都是句子。每个单词都 只 包含大写和小写英文字母。…

K8s 如何通过 ConfigMap 来配置 Redis ?

1、创建 ConfigMap YAML 配置文件 cat <<EOF >./example-redis-config.yaml apiVersion: v1 kind: ConfigMap metadata:name: example-redis-config data:redis-config: "" EOF2、创建 ConfigMap 资源 kubectl apply -f example-redis-config.yaml创建完成…

VMware 已将该虚拟机配置为使用 64 位客户机操作系统。但是,无法执行 64 位操作的解决方法

在电脑上安装VMWare&#xff0c;运行虚拟机发现提示无法执行64位操作。本人系统是windows10,64位系统。错误提示&#xff1a; 已将该虚拟机配置为使用 64 位客户机操作系统。但是&#xff0c;无法执行 64 位操作。 此主机支持 Intel VT-x&#xff0c;但 Intel VT-x 处于禁用状态…

HTML的body元素

&#xff08;1&#xff09;HTML的body元素 body是一个简单的HTML稳定最基本的必需元素。 <body> 标签定义文档的主体。 <body> 元素包含文档的所有内容&#xff08;比如文本、超链接、图像、表格和列表等等&#xff09;。 &#xff08;2&#xff09;HTML 网页结…

HTML零基础教程,九大知识点带你玩转前端(上)

博主&#xff1a;冰小九&#xff0c;新人博主一只&#xff0c;欢迎大佬前来指导 冰小九的主页喜欢请给个三连加关注呀&#xff0c;谢谢&#x1f337;&#x1f337;&#x1f337;三连加关注&#xff0c;追文不迷路&#xff0c;你们的支持就是我最大的动力&#xff01;&#xff0…

【自学Docker 】Docker管理命令大全(上)

文章目录Docker create命令Docker create命令概述Docker create命令语法Docker create命令参数列表案例创建容器运行容器Docker create命令总结Docker exec命令Docker exec命令概述Docker exec命令语法Docker exec命令参数列表案例查看文件创建文件进入容器Docker exec命令总结…

Dubbo 自适应SPI

Dubbo 自适应SPI 1. 原理 在 Dubbo 中&#xff0c;很多拓展都是通过 SPI 机制进行加载的&#xff0c;比如 Protocol、Cluster、LoadBalance 等。有时&#xff0c;有些拓展并不想在框架启动阶段被加载&#xff0c;而是希望在拓展方法被调用时&#xff0c;根据运行时参数进行加…

录屏软件无水印免费,分享一款功能强大且免费的录屏软件

市面上多数录屏软件&#xff0c;只能试用版录制几分钟的视频&#xff0c;且带有水印。想要长时间录制电脑屏幕、录制无水印的录屏&#xff0c;需要解锁才可以。那有没有一款录屏软件试用版就能无水印&#xff1f;当然有啦。小编今天给大家分享一款不限制录制时长&#xff0c;且…

springboot整合Freemarker模板引擎

2.2 模板引擎 2.2.1 什么是模板引擎 根据前边的数据模型分析&#xff0c;课程预览就是把课程的相关信息进行整合&#xff0c;在课程预览界面进行展示&#xff0c;课程预览界面与课程发布的课程详情界面一致&#xff0c;保证了教学机构人员发布前看到什么样&#xff0c;发布后…

【Win11 + VSCode配置OpenCV C++一站式开发调试环境教程】

Win11 VSCode配置OpenCV C一站式开发调试环境教程1 下载1.1 版本介绍&#xff1a;1.2 对应三个软件的连接&#xff1a;2 环境配置3 编译1 下载 需要下载三个软件&#xff1a;OpenCV 、MInGW、CMake 1.1 版本介绍&#xff1a; 打开 OpenCV-MinGW-Build&#xff1a;OpenCV-4.…

Android启动流程源码分析(基于Android S)

从上图我们可以清楚的看到Android系统的启动分为以下几个步骤 启动电源以及启动系统 当我们按下电源键时, 引导芯片代码开始从预定义的地方(固化在ROM)开始执行, 加载引导程序到RAM, 然后执行 引导程序 引导程序是在Android操作系统开始运行前的一个小程序. 引导程序是运行的…

图片转PDF怎么弄?这几个方法值得你试一试

PDF是一种特殊的文件格式&#xff0c;它可以在任何设备和平台上进行传输&#xff0c;并且能够保证文件版式不被修改&#xff0c;此外&#xff0c;还可以兼容不同的系统&#xff0c;因为它的这些优势&#xff0c;大多数的人就喜欢将自己编辑好的WORD、PPT、EXCEL、图片等文件转换…

MySQL InnoDB的MVCC实现机制

MySQL InnoDB的MVCC实现机制1.MVCC概述2.MVCC的实现原理隐式字段undo日志Read View(读视图)RR隔离级别的Read View方案1.MVCC概述 什么是MVCC&#xff1f; MVCC&#xff0c;即多版本并发控制。MVCC是一种并发控制的方法&#xff0c;一般在数据库管理系统中&#xff0c;实现对…

YOLOV8——快速训练指南(上手教程、自定义数据训练)

概述 本篇主要用于说明如何使用自己的训练数据&#xff0c;快速在YOLOV8 框架上进行训练。当前&#xff08;20230116&#xff09;官方文档和网上的资源主要都是在开源的数据集上进行测试&#xff0c;对于算法“小白”或者“老鸟”如何快速应用到自己的项目中&#xff0c;这…

操作系统IO控制方式

操作系统I&O控制方式 视频地址&#xff1a;https://www.bilibili.com/video/BV1YE411D7nH?p64 I&O设备按照信息交换的单位可以分为以下两类&#xff1a; 块设备 数据传输的基本单位是块&#xff0c;传输速率较高&#xff0c;可寻址&#xff0c;可随机读写任意一块。…

78.循环神经网络(RNN)

1. 潜变量自回归模型 2. 循环神经网络 计算损失是比较ot和xt之间来计算损失&#xff0c;但是xt是用来更新ht&#xff0c;使得其挪到下一个单元。 用一个额外的whh来存时序信息。 3. 使用循环神经网络的语言模型 4. 困惑度&#xff08;perplexity&#xff09; 5. 梯度剪裁 g表…

《Stealth秘密行动》游戏开发记录

游戏开发的学习记录项目&#xff1a;Stealth秘密行动开始时间&#xff1a;2022.12.30一、新学到的&#xff1a;二、遇到的问题&#xff1a;三、成品部分展示&#xff1a;游戏开发的学习记录⑧ 项目&#xff1a;Stealth秘密行动 开始时间&#xff1a;2022.12.30 &#xff08;…