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;…

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

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

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

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

每日面经分享(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…

利用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;设备不能被主动扫描…

Kotlin:常用标准库函数(let、run、with、apply、also)

一、let 扩展函数 Kotlin标准库函数let可用于范围确定和空检查。当调用对象时&#xff0c;let执行给定的代码块并返回其最后一个表达式的结果。对象可以通过引用(默认情况下)或自定义名称在块中访问。 let扩展函数源码 let.kt文件代码 fun main() {println("isEmpty $is…

Stable Diffusion扩散模型【详解】小白也能看懂!!

文章目录 1、Diffusion的整体过程2、加噪过程2.1 加噪的具体细节2.2 加噪过程的公式推导 3、去噪过程3.1 图像概率分布 4、损失函数5、 伪代码过程 此文涉及公式推导&#xff0c;需要参考这篇文章&#xff1a; Stable Diffusion扩散模型推导公式的基础知识 1、Diffusion的整体…

【Unity 实用工具篇】| Unity中 实现背景模糊效果,简单易用

前言【Unity 实用工具篇】| Unity 实现背景模糊效果,简单易用一、实现背景模糊效果1.1 介绍1.2 效果展示1.3 使用说明及下载二、插件资源简单介绍2.1 导入下载好的资源2.2 功能介绍2.2.1 捕获特效2.2.2 高级选项

【Vue】watch监听复杂数据,新值与旧值一样

问题 watch监听复杂数据&#xff0c;例如数组&#xff0c;旧值与新值一样 解决方案 监听回调里返回新数组&#xff0c;新、旧数组地址改变&#xff0c;得到的值也就不一样&#xff0c;例↓ ()>[...data] 码 test.js // 数据 const musicList ref([{ id: 540000200805…

00-JAVA基础-动态编译

动态编译 JAVA 6 引入了动态编译机制。Java 动态编译是指在运行时将Java源代码编译成可执行的字节码。这通常使用Java的内置编译器API javax.tools.JavaCompiler 来实现。 动态编译的应用场景 可以做一个浏览器编写java代码&#xff0c;上传服务器编译和运行的在线测评系统服…

继承.Java

目录 1&#xff0c;概述 1.1继承的含义 1.2什么时候用继承 1.3继承的好处 1.4继承的特点 2&#xff0c;继承的格式 3&#xff0c;可以继承哪些内容 4&#xff0c;成员方法和成员变量的访问特点 5&#xff0c;构造方法的访问特点 6&#xff0c;this&#xff0c;super…