C++理解std::move和转发(std::forward)

理解 std::move

标准库move函数是使用右值引用的模板的一个很好的例子。

幸运的是,我们不必理解move所使用的模板机制也可以直接使用它。

但是,研究move是如何工作的可以帮助我们巩固对模板的理解和使用。

我们注意到,虽然不能直接将一个右值引用绑定到一个左值上,但可以用move获得一个绑定到左值上的右值引用。

由于move本质上可以接受任何类型的实参,因此我们不会惊讶于它是一个函数模板。

std:move是如何定义的

标准库是这样定义move的:

//在返回类型和类型转换中也要用到typename
template <typename T>
typename remove_reference<T>::typess move(T&& t)
{return static_cast<typename remove_reference<T>::type&&>(t);
}

这段代码很短,但其中有些微妙之处。

首先,move的函数参数T&&是一个指向模板类型参数的右值引用。通过引用折叠,此参数可以与任何类型的实参匹配。特别是,我们既可以传递给move一个左值,也可以传递给它一个右值,

    string s1("hi!"),s2;s2 = std::move(string("bye!"));// 正确:从一个右值移动数据s2 = std::move(s1);// 正确:但在赋值之后,s1的值是不确定的cout << s1 << endl;//没有打印任何东西


std::move是如何工作的

第一个赋值·

在第一个赋值中,传递给move 的实参是 string的构造函数的右值结果——string("bye!”)。

如我们已经见到过的,当向一个右值引用函数参数传递一个右值时,由实参推断出的类型为被引用的类型。

因此,在std::move(string("bye!"))中:

  • 推断出的T的类型为string。
  • 因此,remove reference用string进行实例化。
  • remove reference<string>的type成员是string。
  • move 的返回类型是string&&。
  • move的函数参数t的类型为string&&。

因此,这个调用实例化move<string>,即函数

string&& move(string &&t)

函数体返回 static cast<string&&>(t)。t的类型已经是string&&,于是类型转换什么都不做。因此,此调用的结果就是它所接受的右值引用。

第二个赋值

现在考虑第二个赋值,它调用了std::move()。

在此调用中,传递给move的实参是一个左值。这样:

  • 推断出的T的类型为string&(string的引用,而非普通string)。
  • 因此,remove_reference用string&进行实例化。
  • remove_reference<string&>的type成员是string。
  • move 的返回类型仍是string&&。
  • move的函数参数t实例化为string&&,会折叠为string&。

因此,这个调用实例化move<string&>,即

string&& move(string &t)

这正是我们所寻求的——我们希望将一个右值引用绑定到一个左值。

这个实例的函数体返回static cast<string&&>(t)。在此情况下,t的类型为string&,cast将其转换为string&&。

从一个左值static_cast到一个右值引用是允许的

通常情况下,static_cast只能用于其他合法的类型转换。

但是,这里又有一条针对右值引用的特许规则:虽然不能隐式地将一个左值转换为右值引用,但我们可以用static cast显式地将一个左值转换为一个右值引用。

对于操作右值引用的代码来说,将一个右值引用绑定到一个左值的特性允许它们截断左值。

实际上,在某些情况下,确实可以使用static_cast来将左值转换为右值引用,尽管这种做法并不常见,并且在实践中很少这样做,因为它会改变原有对象的值类别。

在C++中,static_cast通常不用来直接将左值转换为右值引用,因为这种转换在语义上可能不太明确。然而,如果你确实需要这样做,并且了解这样做的后果,你可以使用static_cast来显式地进行转换。

以下是一个例子,展示了如何使用static_cast将一个左值转换为对应的右值引用:

#include <iostream>  void foo(int&& x) {  std::cout << "Called with rvalue reference to int: " << x << std::endl;  
}  int main() {  int lvalue = 42; // 这是一个左值  foo(static_cast<int&&>(lvalue)); // 使用static_cast将左值强制转换为右值引用  return 0;  
}

这段代码将左值lvalue通过static_cast转换为右值引用,并传递给函数foo。尽管这是合法的,但通常不推荐这样做,因为它可能违反了右值引用的初衷,即处理临时对象或不再需要的资源。

需要注意的是,将左值强制转换为右值引用可能会导致未定义行为,特别是如果转换后的右值引用被用于移动语义,而原左值在之后仍被使用。这是因为移动操作通常会改变对象的内部状态,使得对象处于有效但未定义的状态。

因此,尽管技术上可以这样做,但实践中应该避免无必要地将左值转换为右值引用,除非在特定的上下文中,你完全理解这样做的后果,并且确信这是正确的处理方式。在大多数情况下,使用std::move是更安全、更明确的选择,因为它清楚地表明了对象的值将被“移动”而不是“复制”或“引用”。

转发

某些函数需要将其一个或多个实参连同类型不变地转发给其他函数。

在此情况下,我们需要保持被转发实参的所有性质,包括实参类型是否是const的以及实参是左值还是右值。

作为一个例子,我们将编写一个函数,它接受一个可调用表达式和两个额外实参。

我们的函数将调用给定的可调用对象,将两个额外参数逆序传递给它。

下面是我们的翻转函数的初步模样:

//接受一个可调用对象和另外两个参数的模板
//对“翻转”的参数调用给定的可调用对象
// flip1是一个不完整的实现:顶层const和引用丢失了
template <typename F, typename T1, typename T2>
void flip(F f, T1 t1, T2 t2)
{f(t2,t1);
}

这个函数一般情况下工作得很好,但当我们希望用它调用一个接受引用参数的函数时就会出现问题:

void f(int v1, int& v2) //注意v2是一个引用
{cout << v1 << " " << ++v2 << endl;}

在这段代码中, f改变了绑定到v2的实参的值。

但是,如果我们通过flip调用f,f所做的改变就不会影响实参

template <typename F, typename T1, typename T2>
void flip(F f, T1 t1, T2 t2)
{f(t2,t1);
}
void f(int v1, int& v2) //注意v2是一个引用
{cout << v1 << " " << ++v2 << endl;}int i = 8;int j = 0;f(42, i);//f改变了实参iflip(f, j, 42);//调用filp不会改变jcout << j << endl;

 

问题在于j被传递给flip的参数t1。此参数是一个普通的、非引用的类型int,而非int&
因此,这个flip调用会实例化为
 

void flip1(void(*fcn)(int,int),int t1,int t2);


j的值被拷贝到t1中。f中的引用参数被绑定到t1,而非j,从而其改变不会影响j

定义能保持类型信息的函数参数

为了通过翻转函数传递一个引用,我们需要重写函数,使其参数能保持给定实参的“左造性”。

更进一步,可以想到我们也希望保持参数的const属性。

通过将一个函数参数定义为一个指向模板类型参数的右值引用,我们可以保持其对应实参的所有类型信息。

而使用引用参数(无论是左值还是右值)使得我们可以保持const属性,因为在引用类型中的const是底层的。

如果我们将函数参数定义为T1&&和T2&&,通过引用折叠就可以保持翻转实参的左值/右值属性

template<typename F,typename Tl, typename T2>
void flip2(F f, Tl&& tl, T2&& t2)
{f(t2,t1);
}

与较早的版本一样,如果我们调用flip2(f,j,42),将传递给参数t1一个左值j。

但是,在flip2中,推断出的T1的类型为int&,这意味着t1的类型会折叠为int&。

由于是引用类型,t1被绑定到j上。当flip2调用f时,f中的引用参数v2被绑定到t1,也就是被绑定到1。当f递增v2时,它也同时改变了j的值。

如果一个函数参数是指向模板类型参数的右值引用(如T&&),它对应的实参的const属性和左值/右值属性将得到保持。

这个版本的flip2解决了一半问题。它对于接受一个左值引用的函数工作得很好,但不能用于接受右值引用参数的函数。

例如:

void g(int&& i, int& j)
{cout << i << "" << j << endl;
}


如果我们试图通过flip2调用g,则参数t2将被传递给g的右值引用参数。即使我们传递一个右值给flip2:

flip2(g,i,42);// 错误;不能从一个左值实例化 int&&


传递给g的将是flip2中名为t2的参数。函数参数与其他任何变量一样,都是左值表达式。

因此,flip2中对g的调用将传递给g的右值引用参数一个左值。

在调用中使用std::forward保持类型信息

我们可以使用一个名为forward的新标准库设施来传递flip2的参数,它能保持原始实参的类型。

类似于move,forward定义在头文件utility中。

与move不同,forward必须通过显式模板实参来调用。

forward 返回该显式实参类型的右值引用。即,forward<T>的返回类型是T&&

通常情况下,我们使用forward传递那些定义为模板类型参数的右值引用的函数参数。

通过其返回类型上的引用折叠,forward可以保持给定实参的左值/右值属性

template <typename Type> 
intermediary(Type &&arg)
{
//。。。finalFcn (std::forward<Type>(arg));
{
}


本例中我们使用Type作为forward的显式模板实参类型,它是从arg推断出来的。

由于arg是一个模板类型参数的右值引用,Type将表示传递给arg的实参的所有类型信息。

如果实参是一个右值,则Type是一个普通(非引用)类型,forward<Type>将返回Type&&。

如果实参是一个左值,则通过引用折叠,Type本身是一个左值引用类型。

在此情况下,返回类型是一个指向左值引用类型的右值引用。再次对 forward<Type>的返回类型进行引用折叠,将返回一个左值引用类型。

当用于一个指向模板参数类型的右值引用函数参数(T&a)时,forward会保持实参类型的所有细节。

使用forward,我们可以再次重写翻转函数:

template <typename F, typename Tl, typename T2>
void flip(F f, Tl &&t1, T2 &&t2)
{
f(std::forward<T2>(t2),std::forward<T1>(t1));
}

如果我们调用flip(g, i,42),i将以int&类型传递给g,42将以int&&类型传递给g。

与std::move相同,对std::forward不使用using声明是一个好主意。

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

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

相关文章

提升工作效率:B端工作台设计基础详解

随着互联网和信息技术的快速发展&#xff0c;越来越多的企业开始以数字化、智能化的方式管理和运营自己的业务。B端工作台设计作为企业应用的重要组成部分&#xff0c;越来越受到重视。本文将从三个方面对B端工作台设计进行全面分析。让我们看看。 1. B端工作台设计原则 B端工…

攻防世界 wife_wife

在这个 JavaScript 示例中&#xff0c;有两个对象&#xff1a;baseUser 和 user。 baseUser 对象定义如下&#xff1a; baseUser { a: 1 } 这个对象有一个属性 a&#xff0c;其值为 1&#xff0c;没有显式指定原型对象&#xff0c;因此它将默认继承 Object.prototype。 …

不堪大用的pow

【题目描述】 输出100&#xff5e;999中的所有水仙花数。若3位数ABC满足&#xff0c;则称其为水仙花 数。例如&#xff0c;所以153是水仙花数。 【题目来源】 刘汝佳《算法竞赛入门经典 第2版》习题2-1 水仙花数&#xff08;daffodil&#xff09; 题目很简单&#xff0c;…

AI大模型在未来如何与体育运动结合

AI大模型与体育运动的结合预示着未来体育领域将迎来革命性的变化。这种结合不仅能提升运动员的训练和比赛表现&#xff0c;还能改善观众的观赛体验&#xff0c;优化体育管理和运营。以下是几个预期中AI大模型将如何与体育运动结合的方向&#xff1a; 1. 运动员表现分析与提升 …

配置vite配置文件更改项目端口、使用@别名

一、配置vite配置文件更改项目端口 vite官方文档地址&#xff1a;开发服务器选项 | Vite 官方中文文档 (vitejs.dev) 使用&#xff1a; 二、使用别名 1. 安装 types/node types/node 包允许您在TypeScript项目中使用Node.js的核心模块和API&#xff0c;并提供了对它们的类型…

C语言经典例题(17) --- 最小公倍数、单词倒置、你是天才吗?、完美成绩、判断整数的奇偶性

1.最小公倍数 正整数A和正整数B的最小公倍数是指能被A和B整除的最小的正整数&#xff0c;设计一个算法&#xff0c;求输入A和B的最小公倍数。 输入描述&#xff1a;输入两个正整数A和B。 输出描述&#xff1a;输出A和B的最小公倍数。 输入&#xff1a;5 7 输出&#xff1a…

4. python练习题4-水仙花数

4. python练习题4-水仙花数 【目录】 文章目录 4. python练习题4-水仙花数1. 目标任务2. 水仙花数的特点3. 如何判断一个数是否是水仙花数&#xff1f;4. 打印3位水仙花数5. 判断一个数是不是水仙花数6. 列表推导式6. 列表推导式判断一个数是不是水仙花数 【正文】 1. 目标任务…

picGo图床搭建gitee和smms(建议使用)

picGoGitee 这个需要下载gitee插件, 因为官方频繁的检索文件类型, 有时候也会失效 如果没有特殊要求平时存个学习的要看图中文字的重要的图片建议就是smms, 免费也够用! 图片存本地不方便, 各种APP中来回传还会失帧损失画质, 所以你值得往下看 picGosmms 建议使用这个, sm…

代码随想录-算法训练营day01【二分查找、移除元素、插入元素】

专栏笔记&#xff1a;https://blog.csdn.net/weixin_44949135/category_10335122.html 第一章 数组part01今日任务 数组理论基础&#xff0c;704. 二分查找&#xff0c;27. 移除元素 详细布置数组理论基础 文章链接&#xff1a;https://programmercarl.com/%E6%95%B0%E7%BB%…

递归与树的深度优先搜索:探索它们之间的关系

文章目录 递归与树的深度优先搜索&#xff1a;探索它们之间的关系递归的基本概念树的深度优先搜索递归与树的深度优先搜索的关系反转链表合并两个有序链表 总结 递归与树的深度优先搜索&#xff1a;探索它们之间的关系 递归是一种强大而优雅的编程技术,它允许我们通过将问题分…

每日面经分享(python part1)

Python中的深拷贝和浅拷贝的区别是什么&#xff1f; a. 浅拷贝创建一个新的对象&#xff0c;但其中的可变元素仍然共享引用。只有对象的第一层被复制&#xff0c;而更深层次的嵌套对象仍然是引用。更改其中一个对象的属性会影响到其他对象。 b. 深拷贝创建一个完全独立的新对象…

docker部署nacos,单例模式(standalone),使用内置的derby数据库,简易安装

文章目录 前言安装创建文件夹docker指令安装docker指令安装-瘦身版 制作docker-compose.yaml文件查看页面 前言 nacos作为主流的服务发现中心和配置中心&#xff0c;广泛应用于springcloud框架中&#xff0c;现在就让我们一起简易的部署一个单例模式的nacos&#xff0c;版本可…

【Linux】详解动态库链接和加载对可执行程序底层的理解

一、动静态库链接的几种情况 如果我们同时提供动态库和静态库&#xff0c;gcc默认使用的是动态库。如果我们非要使用静态库&#xff0c;要加-static选项。如果我们只提供静态库&#xff0c;那可执行程序没办法&#xff0c;只能对该库进行静态链接&#xff0c;但程序不一定整体…

python和pip中常见命令和方法

玩python的同学想必没有不用pip的吧&#xff0c;pip是python包管理工具&#xff0c;和Nodejs的npm、Java的maven类似&#xff0c;这些依靠开源力量建立起的庞大软件库极大提高了开发的效率&#xff0c;下面是整理和总结pip中的常见命令和方法。 pip更新版本 python -m pip inst…

【架构二】胖瘦客户端

瘦客户端和胖客户端是指在客户端-服务端架构中&#xff0c;客户端所承担的功能和责任不同。瘦客户端是指在客户端设备上&#xff0c;主要负责显示用户界面和处理用户输入&#xff0c;而大部分的应用逻辑和数据处理都在服务端完成。瘦客户端通常只需要较少的计算能力和存储资源&…

利用Spark将Kafka数据流写入HDFS

利用Spark将Kafka数据流写入HDFS 在当今的大数据时代&#xff0c;实时数据处理和分析变得越来越重要。Apache Kafka作为一个分布式流处理平台&#xff0c;已经成为处理实时数据的事实标准。而Apache Spark则是一个强大的大数据处理框架&#xff0c;它提供了对数据进行复杂处理…

Echarts 自适应宽高,或指定宽高进行自适应

文章目录 需求分析 需求 有一个按钮实现对Echarts的指定缩放与拉长&#xff0c;形成自适应效果 拉长后效果图 该块元素缩短后效果图 分析 因为我习惯使用 ref 来获取组件的 DOM 元素&#xff0c;然后进行挂载 <div ref"echartsRef" id"myDiv" :sty…

OCR常用识别算法综述

参考&#xff1a;https://aistudio.baidu.com/education/lessonvideo/3279888 语种&#xff1a;常用字符36与常用汉字6623&#xff0c;区别。 标注&#xff1a;文本型位置/单字符位置&#xff0c;后者标注成本大 挑战&#xff1a;场景文字识别&#xff1a;字符大小、颜色、字体…

力扣1379---找出克隆二叉树的相同节点(Java、DFS、简单题)

目录 题目描述&#xff1a; 思路描述&#xff1a; 代码&#xff1a; &#xff08;1&#xff09;&#xff1a; &#xff08;2&#xff09;&#xff1a; 题目描述&#xff1a; 给你两棵二叉树&#xff0c;原始树 original 和克隆树 cloned&#xff0c;以及一个位于原始树 ori…

蓝牙学习九(定向广播 ADV_DIRECT_IND)

一、简介 广播类型有如下&#xff1a; 非定向可连接广播&#xff08;ADV_IND&#xff09;。可连接的非定向广播&#xff0c;表示当前设备可以接受任何设备的连接请求。 定向可连接广播&#xff08;ADV_DIRECT_IND&#xff09;。可连接的定向广播&#xff0c;设备不能被主动扫描…