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改变了实参i
    flip(f, j, 42);//调用filp不会改变j
    cout << 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声明是一个好主意。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【C语言】linux内核pci_register_driver

一、注释 以下是对源代码中英文注释的中文翻译&#xff0c;可能会略去一些编程上的专有词汇&#xff08;例如函数名、类型名等&#xff09;&#xff0c;以使翻译更易理解。 // drivers\pci\pci-driver.c /*** __pci_register_driver - 注册一个新的PCI驱动* drv: 需要注册的驱…

【QT入门】 无边框窗口设计综合运用之自定义标题栏带圆角阴影的窗口

往期回顾&#xff1a; 【QT入门】 自定义标题栏界面qss美化按钮功能实现-CSDN博客 【QT入门】 无边框窗口设计之实现窗口阴影-CSDN博客 【QT入门】 无边框窗口设计之实现圆角窗口-CSDN博客 【QT入门】 无边框窗口设计综合运用之自定义标题栏带圆角阴影的窗口 一、最终效果 二、…

数据结构进阶篇 之 【交换排序】(冒泡排序,快速排序递归、非递归实现)

当你觉的自己不行时&#xff0c;你就走到斑马线上&#xff0c;这样你就会成为一个行人 一、交换排序 1.冒泡排序 BubbleSort 1.1 基本思想 1.2 实现原理 1.3 代码实现 1.4 冒泡排序的特性总结 2.快速排序 QuickSort 2.1 基本思想 2.2 递归实现 2.2.1 hoare版 2.2.2 …

开发环境->生产环境

1、数据迁移 不涉及docker # 以数据库用户导出数据 mysqldump -h 192.168.1.168 -P 3307 -u abragent -pabragebb17 abragent > abragent.sql# 以root用户导出数据 mysqldump -h 192.168.1.168 -P 3307 -u root -p8d3Ba1b abragent > abragent.sql 涉及docker …

java自动化学习-IntelliJ IDEA新建项目

1、新建项目 2、新建类&#xff0c;右键”src” > “new” >”Java Class” 3、重命名类名

【史上最细教程】项目本地切换Nexus私服步骤

文章目录 1.上传所有jar/pom到私服仓库方式1&#xff1a;Nexus手动上传方式2&#xff1a;mvn deploy命令上传 2.替换项目中所有pom.xml上传下载地址为私服仓库3.替换本地maven setting.xml配置文件4.下载上传验证操作下载jar出现的问题mvn deploy上传jarmvn deploy上传执行脚本…

R语言实现蒙特卡洛模拟算法

&#x1f349;CSDN小墨&晓末:https://blog.csdn.net/jd1813346972 个人介绍: 研一&#xff5c;统计学&#xff5c;干货分享          擅长Python、Matlab、R等主流编程软件          累计十余项国家级比赛奖项&#xff0c;参与研究经费10w、40w级横向 文…

java 数据结构 Map和Set

目录 搜索树 操作-查找 操作-插入 操作-删除&#xff08;难点&#xff09; Map Map 的常用方法 Set 哈希表 哈希函数 哈希冲突 冲突-避免-负载因子调节&#xff08;重点掌握&#xff09; 冲突-解决 冲突-解决-开散列/哈希桶(重点掌握) 实现HashBuck类 put方法 …

C++实现 “你被骗了” 自动拦截,反诈神器

“Never Gonna Give You Up” &#xff0c; 已经是历经十五年的名梗了&#xff0c;点开这个视频&#xff0c;就说明 你被骗了。 无论是自己点进了一些奇奇怪怪的链接&#xff0c;还是被自动跳转&#xff0c;你都不希望 展开 0x01 原理&规则 【本程序B站视频链接】快去B站…

layui框架实战案例(26):layui-carousel轮播组件添加多个Echarts图标的效果

在Layui中&#xff0c;使用layui-carousel轮播组件嵌套Echarts图表来实现多个图表的展示。 css层叠样式表 调整轮播图背景色为白色&#xff1b;调整当个Echarts图表显示loading…状态&#xff1b;同一个DIV轮播项目添加多个Echarts的 .layui-carousel {background-color: #f…

【图论】有向无环图中一个节点的所有祖先 - 邻接表(DFS)

文章目录 题目&#xff1a;有向无环图中一个节点的所有祖先题目描述代码与解题思路 题目&#xff1a;有向无环图中一个节点的所有祖先 2192. 有向无环图中一个节点的所有祖先 题目描述 代码与解题思路 func getAncestors(n int, edges [][]int) [][]int {g : make([][]int, …

C#清空窗体的背景图片

目录 一、涉及到的知识点 1.设置窗体的背景图 2.加载窗体背景图 3.清空窗体的背景图 二、 示例 一、涉及到的知识点 1.设置窗体的背景图 详见本文作者的其他文章&#xff1a;C#手动改变自制窗体的大小-CSDN博客 https://wenchm.blog.csdn.net/article/details/137027140…

链路追踪原理

分布式系统为什么需要链路追踪&#xff1f; 随着互联网业务快速扩展&#xff0c;软件架构也日益变得复杂&#xff0c;为了适应海量用户高并发请求&#xff0c;系统中越来越多的组件开始走向分布式化&#xff0c;如单体架构拆分为微服务、服务内缓存变为分布式缓存、服务组件通…

IDEA2023.1.1中文插件

1.启动IDEA 选中Customize 2.选择All settings 3.选中Plugins,再搜索栏里输入Chinese,找到 "Chinese (Simplified) Language"插件&#xff0c;点击 Install 进行安装。 4. 安装完成后&#xff0c;重启IntelliJ IDEA&#xff0c;即可看到界面语言已经变为中文。

HashMap为啥线程不安全?

1. HashMap1.7在多线程并发下扩容时&#xff0c;头插法会出现环。 /*** Rehashes the contents of this map into a new array with a* larger capacity. This method is called automatically when the* number of keys in this map reaches its threshold.** If current cap…

回溯算法|491.递增子序列

力扣题目链接 class Solution { private:vector<vector<int>> result;vector<int> path;void backtracking(vector<int>& nums, int startIndex) {if (path.size() > 1) {result.push_back(path);// 注意这里不要加return&#xff0c;要取树上…

[计算机知识] TCP/IP网络模型、MySQL的结构

TCP/IP网络模型 应用层 给用户提供应用功能&#xff0c;如HTTP, DNS 应用层处于用户态&#xff0c;传输层及以下处于内核态 传输层 给应用层提供网络支持&#xff0c;如TCP, UDP TCP提供稳定、面向连接的网络传输协议 应用层的数据可能会太大&#xff0c;就需要进行拆分…

【GAMES101】Lecture08 09 Shading 3 (Texture Mapping cont.) 纹理映射 续集

目录 0 引言1 如何在三角形内进行插值&#xff1a;重心坐标1.1 为什么要在三角形内插值1.2 重心坐标1.3 使用重心坐标做三角形内颜色插值 2 应用纹理2.1 简单的纹理映射&#xff1a;漫反射2.2 问题&#xff1a;纹理放大&#xff08;采用插值解决&#xff09;2.2 点查询和范围查…

Qt主窗口 之:停靠/悬浮窗口(QDockWidget)

一、QDockWidget概述 QDockWidget 是 Qt 中的一个窗口部件&#xff0c;用于创建可停靠的窗口&#xff0c;通常用于构建多文档接口&#xff08;MDI&#xff09;或可定制的用户界面。QDockWidget 允许用户将窗口停靠在应用程序的主窗口周围&#xff0c;或将其拖动到独立的浮动窗…

STM32

GPIO通用输入输出口 GPIO:8种输入输出模式 浮空输入可读取引脚电平&#xff0c;若引脚悬空&#xff0c;电平不确定上拉输入可读取引脚电平&#xff0c;内部接上拉电阻&#xff0c;悬空时默认为高电平下拉输入可读取引脚电平&#xff0c;内部接下拉电阻&#xff0c;悬空时默认…