模板中的右值引用(万能引用)、引用折叠与完美转发

news2024/11/20 21:21:59

模板中的右值引用(万能引用)、引用折叠与完美转发

文章目录

  • 模板中的右值引用(万能引用)、引用折叠与完美转发
    • 一、万能引用与引用折叠
      • 1. 模板中的右值引用
      • 2. 自动类型推导(auto)与万能引用
      • 3. 引用折叠与万能引用
      • 4. lambda表达式捕获
      • 5. 条件转发
      • 6. 类型萃取
    • 二、完美转发
    • 总结

一、万能引用与引用折叠

1. 模板中的右值引用

​ 我们经常听万能引用,什么是万能引用,与普通的右值引用有什么区别?

下面给出一个案例,利用模板函数参数中的右值引用来展现万能引用的用途:

首先给出一个自定义的类型,简易的实现出拷贝构造函数和移动构造函数同时不必关注是否正常实现了功能,增加打印信息以便我们可以通过控制台明晰该函数是否被调用,何时被调用:

class Date
{
public:
	Date(int year, int month, int day)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date(int year, int month, int day)" << endl;
	}

	Date(const Date& d)
	{
		cout << "Date(const Date& d)" << endl;
	}
	Date(Date&& d)
	{
		cout << "Date(Date&& d)" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

接着给出两个测试函数,函数参数类型分别常量左值引用和右值引用:

// 常量左值引用函数模板
template<typename T>
void func1(const T& val)
{
	T d(val);
}
// 万能引用参数函数模板
template<typename T>
void func2(T&& val)
{
	T d(std::forward<T>(val));
	cout << "val是右值引用:" << std::is_rvalue_reference<decltype(val)>::value << endl;
}

注意上面 func2() 函数中用到的 forward() 相当于 move() 的进阶版,可以保证在函数间参数传递时保持原有左/右值类型。

继续给出测试用例:

Date d(2000, 1, 2);

cout << "*********左值参数********" << endl;
func1(d);
cout << "*************************" << endl;
func2(d);

cout << "*********右值参数********" << endl;
func1(std::move(d));
cout << "*************************" << endl;
func2(std::move(d));

运行结果:

在这里插入图片描述

通过上图我们认识到万能引用在模板中的重要性,万能引用是模板中大多数情况保证移动构造函数被正常调用的重要条件。只有参数是万能引用时,函数内部才可调用 std::forward<T>() 完美转发变量的左右值类型,并将保持源类型的变量转发给其他函数。

至于我们为什么称其为“万能引用”,虽然常量左值引用 “const T& val” 也能兼容接收左值和右值参数,但它不支持移动语义,因为它不允许修改绑定的对象因此,万能引用更有优势,因为它既支持移动语义,又能与完美转发结合使用,成功传递变量及其类型。

2. 自动类型推导(auto)与万能引用

当使用auto关键字时,万能引用可以帮助推导变量的类型。例如,auto&&可以根据初始化表达式是左值还是右值来推导出正确的类型。

void test2()
{
	Date d(2000, 1, 2);

	auto&& d1(d);
	cout << "d1是左值引用:" << std::is_lvalue_reference<decltype(d1)>::value << endl;
	cout << "d1是右值引用:" << std::is_rvalue_reference<decltype(d1)>::value << endl;
	auto&& d2(std::move(d));
	cout << "d2是左值引用:" << std::is_lvalue_reference<decltype(d2)>::value << endl;
	cout << "d2是右值引用:" << std::is_rvalue_reference<decltype(d2)>::value << endl;
}

在这里插入图片描述

通过上图我们发现 auto&& 可以在推导表达式类型时,同时推导其左值性或右值性。

3. 引用折叠与万能引用

引用折叠的规则解决了引用的引用(C++中不允许)的问题。这里是引用折叠的基本规则:

  • 如果两个引用中至少一个是左值引用(&),那么结果是左值引用(&)。
  • 如果两个引用都是右值引用(&&),那么结果是右值引用(&&)。

首先给出一个简单的以万能引用为参数的模版函数:

template<typename T>
void func(T&& val) {
	// val 是一个万能引用
	auto p = &val;
	cout << "左值引用:" << std::is_lvalue_reference<decltype(val)>::value << endl;
	cout << "右值引用:" << std::is_rvalue_reference<decltype(val)>::value << endl;
}

测试函数:

void test3()
{
	int x = 10;
	int& lx = x; // lx 是 x 的左值引用
	int&& rx = 10; // rx 是一个右值引用

	func(x);  // T 推导为 int&,因此 val 的类型为 int& &,折叠为 int&
	cout << "x是右值引用:" << std::is_rvalue_reference<decltype(x)>::value << endl;
	cout << "-------------------" << endl;
	func(lx); // T 推导为 int&,因此 val 的类型为 int& &,折叠为 int&
	cout << "lx是右值引用:" << std::is_rvalue_reference<decltype(lx)>::value << endl;
	cout << "-------------------" << endl;
	func(rx); // T 推导为 int&&,因此 val 的类型为 int&& &&,折叠为 int&&
	cout << "xr是右值引用:" << std::is_rvalue_reference<decltype(rx)>::value << endl;
	cout << endl;
	func(move(x));
	cout << "move(x)是右值引用:" << std::is_rvalue_reference<decltype(move(x))>::value << endl;
	cout << "-------------------" << endl;
	func(10); // T 推导为 int,因此 val 的类型为 int&&
}

运行结果:

在这里插入图片描述

对上面运行结果部分地方需要着重做出解释:

在这里插入图片描述

4. lambda表达式捕获

void test4()
{
	auto x = 5;
	auto lambda = [y = std::move(x)]() mutable {
		y += 2;
		return y;
	};

	cout << x << endl;
	cout << lambda() << endl;
}

当调用 lambda() 时,将输出 7,因为 y 被初始化为 5 并且增加了 2。请注意,由于 x 被移动到 yx 的值在移动后未定义,但通常在实际编译器实现中,基本类型的值在移动后保持不变。因此,输出 x 的值仍然是 5。

5. 条件转发

template<typename T>
void forwarder(T&& arg) {
	if constexpr (std::is_lvalue_reference_v<T>) {
		//process(arg); // 处理左值
	}
	else {
		//process(std::move(arg)); // 处理右值
	}
}

在这个例子中,forwarder 函数使用万能引用 T&& 来接受任何类型的参数,并根据参数的类型来决定调用哪个 process 函数。

6. 类型萃取

类型萃取(Type Traits)是模板元编程中的一种技术,它允许你在编译时检查类型信息或者修改类型。万能引用可以与类型萃取结合使用,以确定传递给模板的参数类型的属性。

template<typename T>
void process(T&& arg) {
	using Type = typename std::remove_reference<T>::type;
	if constexpr (std::is_integral_v<Type>) {
		// 如果 T 是整数类型
	//handle_integral(std::forward<T>(arg));
	}
	else {
		// 如果 T 不是整数类型
	//handle_non_integral(std::forward<T>(arg));
	}
}

在这个例子中,process 函数使用类型萃取来移除引用,并检查 T 是否为整数类型。然后它使用 std::forward 来保持参数的值类别,并将其传递给相应的处理函数。

二、完美转发

在这里插入图片描述

​ 实际上这里才真正提起完美转发这个概念,看了本文前面内容也已经大概了解,我们这里仅仅将其抽离出来进行特别总结。

  • 首先,我们已经知道右值引用变量具有左值属性,因为其需要保留可修改性,所以自然不能是右值。

  • 其次,传递给右值引用类型的参数在传参后会退化为左值

所以我们想要在万能引用作参数的函数内部实现分离操作,需要对参数的左右值属性进行判断,例如:

template<typename T>
void print_right(T&& v)
{
	cout << "right -> " << v << endl;
}
template<typename T>
void print_left(const T& v)
{
	cout << "left -> " << v << endl;
}

template<typename T>
void do_something(T&& val)
{
	if (std::is_rvalue_reference<decltype(val)>::value)	// val是右值
	{
		print_right(std::forward<T>(val));	// 利用完美转发将退化逆向为初始类型,此处判断初始右值,也可 move(val)
	}
	else	// val是左值
	{
		print_left(val);		// val退化为左值,直接传递
	}
}

测试函数:

void test5()
{
	int x = 18;
	//do_something<int>(x);		// 编译报错
	do_something<int&>(x);	// 注意这里x是左值,所以模板参数为int编译会报错,模板参数和函数参数类型需要统一满足引用折叠
	do_something<int>(10);
}

注意上面测试代码的案例也是引用折叠的重要体现

对于完美转发 forward(val) 中 val 的属性可能是左值也可能是右值:

  • 当 val 是左值时,forward 对左值 val 不做处理
  • 当 val 是右值时,forward 的作用相当于 move(val)

在这里插入图片描述


总结

​ 本文被三个词语所贯穿:万能引用、引用折叠、完美转发。这三个概念之间的联系紧密以及使用场景高度重合,正是因为C++11中提出如此富有意义的新概念,极大地方便了我们重构代码,理想高效地编码实现功能。

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

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

相关文章

巨某量引擎后台登录实战笔记 | Playwright自动化框架

前言 本文章中所有内容仅供学习交流&#xff0c;抓包内容、敏感网址、数据接口均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff0c;若有侵权&#xff0c;请联系我立即删除&#xff01; 入正题看看滑块是怎么个事…

数字孪生项目开发流程

数字孪生&#xff08;Digital Twin&#xff09;项目的开发流程涉及多个步骤&#xff0c;从初始概念到最终部署和维护。以下是一个典型的数字孪生项目开发流程&#xff0c;通过这些步骤&#xff0c;开发团队可以有效地规划、设计、开发和维护数字孪生项目&#xff0c;确保其在实…

YOLOv5 Exception: Dataset not found.

在使用yolo v5训练时弹出了这个报错&#xff0c;就是没有找到数据集&#xff0c;dataset.yaml文件里面的train 和val 的路径配置不对&#xff0c;开始我是使用相对路径&#xff0c;后面修改成绝对路径就可以了

Ubuntu23.04开机时whoopsie-upload-all占用CPU 100%,风扇狂转

Ubuntu23.04开机时&#xff0c;风扇狂转散热&#xff0c;打开终端&#xff0c;输入top -c&#xff0c;查看占用cpu最高的进程&#xff0c;发现是python3在执行whoopsie-upload-all脚本文件。 什么是whoopsie&#xff1f; 这是“Ubuntu错误报告”守护程序&#xff0c;默认安装在…

DAB协议解读ETSI TS 103 461

一、说明 数字信号广播&#xff08; Digital Audio Broadcasting 简称DAB&#xff09;是继AM、FM传统模拟广播之后的第三代广播--数字信号广播&#xff0c;它提供了接近CD质量的声音&#xff0c;广播及商机无限的附加数据服务&#xff0c;具有抗噪声、抗干扰、抗电波传播衰落、…

采用LoRA方法微调llama3大语言模型

文章目录 前言一、Llama3模型简介1.下载llama3源码到linux服务器2.安装依赖3.测试预训练模型Meta-Llama-3-8B4.测试指令微调模型Meta-Llama3-8B-Instruct5.小结 二、LoRA微调Llama31.引入库2.编写配置文件3.LoRA训练的产物 三、测试新模型效果1.编写配置文件2.运行配置文件&…

(Qt) 默认QtWidget应用包含什么?

文章目录 ⭐前言⭐创建&#x1f6e0;️选择一个模板&#x1f6e0;️Location&#x1f6e0;️构建系统&#x1f6e0;️Details&#x1f6e0;️Translation&#x1f6e0;️构建套件(Kit)&#x1f6e0;️汇总 ⭐项目⚒️概要⚒️构建步骤⚒️清除步骤 ⭐Code&#x1f526;untitled…

Arcpy安装和环境配置

一、前言 ArcPy 是一个以成功的arcgisscripting 模块为基础并继承了arcgisscripting 功能进而构建而成的站点包。目的是为以实用高效的方式通过 Python 执行地理数据分析、数据转换、数据管理和地图自动化创建基础。该包提供了丰富纯正的 Python 体验&#xff0c;具有代码自动…

思维导图-VPN

浏览器集成了受信任的机构的证书

解决word里加入mathtype公式后行间距变大

1.布局>页面设置>文档网格&#xff0c;网格栏选为无网格 2.固定间距

数据库|基于T-SQL创建数据库

哈喽&#xff0c;你好啊&#xff0c;我是雷工&#xff01; SQL Server用于操作数据库的编程语言为Transaction-SQL,简称T-SQL。 本节学习基于T-SQL创建数据库。以下为学习笔记。 01 打开新建查询 首先连接上数据库&#xff0c;点击【新建查询】打开新建查询窗口&#xff0c; …

Linux基础命令[27]-gpasswd

文章目录 1. gpasswd 命令说明2. gpasswd 命令语法3. gpasswd 命令示例3.1 不加参数3.2 -a&#xff08;将用户加入组&#xff09;3.3 -d&#xff08;从组中删除用户&#xff09;3.4 -r&#xff08;删除组密码&#xff09;3.5 -M&#xff08;多个用户一起加入组&#xff09;3.6 …

23种设计模式(持续输出中)

一.设计模式的作用 设计模式是软件从业人员长期总结出来用于解决特定问题的通用性框架&#xff0c;它提高了代码的可维护性、可扩展性、可读性以及复用性。 二.设计模式 1.工厂模式 工厂模式提供了创建对象的接口&#xff0c;而无需制定创建对象的具体类&#xff0c;工厂类…

kafka集群跨区域跨集群同步方案MirrorMaker1 —— 筑梦之路

MirrorMaker原理架构 数据流向 上图也是一种比较常见的用法&#xff0c;这里作为记录。下面介绍一则实战案例。 网络架构 配置日志采集器filebeat 配置从哪里采集日志 输出到kafka集群 配置MirrorMaker消费者 参数说明&#xff1a; bootstrap.servers 指定消费哪个kafka的数…

【HarmonyOS4学习笔记】《HarmonyOS4+NEXT星河版入门到企业级实战教程》课程学习笔记(八)

课程地址&#xff1a; 黑马程序员HarmonyOS4NEXT星河版入门到企业级实战教程&#xff0c;一套精通鸿蒙应用开发 &#xff08;本篇笔记对应课程第 15 节&#xff09; P15《14.ArkUI组件-状态管理state装饰器》 回到最初的 Hello World 案例&#xff0c;首先验证 如果删掉 State…

Day22:Leetcode:654.最大二叉树 + 617.合并二叉树 + 700.二叉搜索树中的搜索 + 98.验证二叉搜索树

LeetCode&#xff1a;654.最大二叉树 1.思路 解决方案&#xff1a; 单调栈是本题的最优解&#xff0c;这里将单调栈题解本题的一个小视频放在这里 单调栈求解最大二叉树的过程当然这里还有leetcode大佬给的解释&#xff0c;大家可以参考一下&#xff1a; 思路很清晰&#xf…

软件开源协议与QT的开源协议介绍

一.常见的六种开源协议 1.BSD协议 BSD协议全称为“Berkely Software Distribution”&#xff0c;中文译为“伯克利软件发行版”。其最早用于伯克利UNIX操作系统上的开源贡献。 主要特点&#xff1a; 允许修改源码 允许源码再发布 允许商业软件发布和销售 约束&#xff1…

JVM学习-垃圾回收(二)

标记-清除(Mark-Sweep)算法 当堆中的有效内存空间被耗尽的时候&#xff0c;就会停止整个程序(stop the world)&#xff0c;然后进行两项工作&#xff0c;第一项则是标记&#xff0c;第二项是清除 标记&#xff1a;Collector从引用根节点开始遍历&#xff0c;标记所有被引用的…

fork 与 vfork 的区别

关键区别一&#xff1a; vfork 直接使用父进程存储空间&#xff0c;不拷贝。 关键区别二&#xff1a; vfork保证子进程先运行,当子进程调用exit退出后&#xff0c;父进程才执行。 我们可以定义一个cnt&#xff0c;在子进程中让它变成3&#xff0c; 如果使用fork&#xff0c;那…

java 8--Lambda表达式,Stream流

目录 Lambda表达式 Lambda表达式的由来 Lambda表达式简介 Lambda表达式的结构 Stream流 什么是Stream流&#xff1f; 什么是流呢&#xff1f; Stream流操作 中间操作 终端操作 Lambda表达式 Lambda表达式的由来 Java是面向对象语言&#xff0c;除了部分简单数据类型…