C++进阶,一文带你彻底搞懂左右值引用以及移动语义和完美转发!

目录

  • 一、左值引用
    • 1.左值
    • 2.左值引用
    • 3.左值引用的用途
      • (1)修改实参
      • (2)减少拷贝
      • (3)使用左值引用可以在外部修改对象内的成员变量的值
  • 二、右值引用
    • 1.右值
      • (1)纯右值
      • (2)将亡值
    • 2.右值引用
  • 三、移动语义
    • 1.std::move()
    • 2.移动语义的使用
    • 3.移动语义的注意点:
      • (1)std::move()本身不移动
      • (2)移动语义是一种规范
  • 四、完美转发
    • 1.引用折叠
      • (1)两个左值引用(&)结合:它们会折叠成一个左值引用(&)。
      • (2)左值引用(&)和右值引用(&&)结合:它们会折叠成一个左值引用(&)。
      • (3)两个右值引用(&&)结合:它们会折叠成一个右值引用(&&)。
      • (3)非引用类型与右值引用(&&)结合:在模板参数推导中,非引用类型与右值引用结合时,会推导出右值引用类型。
    • 2.完美转发的简单实现
    • 3.完美转发的标准实现
  • 五、总结

一、左值引用

左值引用,顾名思义即左值的引用。在详细介绍左值引用之前,我们先探讨一下什么是左值。

1.左值

左值是一个表示对象内存位置的表达式,它出现在赋值操作符的左边。也就是说它有一个持久的身份,并且可以通过地址访问。例如,变量、数组元素、结构体的成员等都是左值。一般来说,在表达式左边的值就是左值。更通俗来说,在大多数情况下,能够进行取地址操作的值,就是左值。(同时左值也一定能够被取地址)

int a = 10; //a是左值
int b = a;  // b是左值
int c = std::max(3, 4); // c是左值
int* p = &a; // p是左值

2.左值引用

左值引用就是对左值的引用。左值引用的语法十分简单,只需要在类型后加上“&”即可,形如T& 你可以将一个左值赋值给一个左值引用

int a = 10; //a是左值
int& b = a; //b 是左值引用
int& c = 10; // 报错,这里10是字面量,是右值,不能直接赋给左值引用

3.左值引用的用途

(1)修改实参

左值引用可以起到类似于指针的效果,在函数拷贝传参后,对左值引用的值做出修改会改变原来的值

void add(int& a) {a += 10;
}
int main() {int a = 10;add(a);std::cout << a << std::endl; // 20return 0;
}

在函数中修改实参的值,也是左值引用的一大功能之一

(2)减少拷贝

减少拷贝的作用其实和指针类似,当一个数据类型占用内存量过大时,我们就可以通过传递该类型的左值引用来减少拷贝提高性能。常见于函数传参,尤其是,类的拷贝构造函数。

struct A {int* _data{};A() : _data(new int[100]) {}A(const A& other) { //拷贝构造函数,使用常量左值引用,可以减少拷贝(因为有const修饰所以无法修改该引用的值)//todo}~A() {delete[] _data;}
};void acquire_A(A& a) { //普通函数传参时,使用左值引用可以减少拷贝// todo
}

当构造函数的形参为常量左值引用时(const T& a),c++会将其认为是拷贝构造函数,当构造对象传入的参数为该类的左值时,默认调用该拷贝构造函数。

(3)使用左值引用可以在外部修改对象内的成员变量的值

使用左值引用,我们就可以通过成员函数获取成员变量的值,并且修改获取的值时,也会影响原有成员变量的值

struct A {int _data{};A(int a) : _data(a) {}int& get_data() {return _data;}
};int main() {A a{9};std::cout << a._data << std::endl; // 9int& t = a.get_data(); // 使用左值引用来接收t = 90; // 在外部改变引用的值std::cout << a._data << std::endl; // 90 说明成员变量的值也被改变了int t2 = a.get_data(); // 不使用左值引用来接收值t2 = 100; // 改变值std::cout << a._data << std::endl; // 90 说明没有改变成员变量的值return 0;
}

这里需要特殊说明的是,当一个类或结构体的成员函数返回一个左值引用时,会出现可以直接给函数赋值一样的情况。

 	a.get_data() = 23;std::cout << a._data << std::endl; // 23

我们知道,函数返回的是一个右值(这个在介绍右值是会详细说明),但右值是无法被赋值的。这里之所以能直接”给函数赋值“,是因为函数返回的是左值引用,引用了_data,本质是在给_data赋值





二、右值引用

右值引用就是对右值的引用,在介绍右值引用之前,需要对右值有个清晰的理解。所以我们先来认识一下右值。

1.右值

右值,即表达式右边的值,右值不能被取地址,也就是说不能被赋值。右值又可以被分为纯右值(Prvalues)和将亡值(Xvalues)
值的分类

右值通常表示临时对象字面量或者不需要或不应该有持久性存储位置的其他值。从C++11开始,右值被进一步细分为纯右值和将亡值,但通常我们简单地用“右值”来统称这两类。

(1)纯右值

纯右值包括:
字面量(如 423.14
临时对象(如表达式 std::string("hello") + " world" 的结果)
不与任何对象关联的结果(如非引用返回的函数的返回值、类型转换表达式的结果等)

需要额外注意的一点是,并不是所有的字面量都是右值,例如字符串字面量就不是右值,因为单纯的字符串字面量是可以取值的(本质是因为字符串字面量是常量)

(2)将亡值

将亡值,也叫做“右值引用表达式”,是C++11中引入的新概念,主要包括:
移动操作的源对象(如 std::move(obj) 的结果)
返回值是右值引用的函数返回的表达式

以下的例子都为右值:

a++; a--; //注意,前置自增和前置自减是左值
a + b; a << b; a > b; a && b; !a// 表达式
a[n]; //数组下标
[&](int a) {return a * 2;}//lambda表达式
&a;//取地址
this->a
p->a
A();//构造函数

2.右值引用

右值引用的语法同样十分简单,其语法为在数据类型之后加上"&&",即类似于T&&的形式。

int get_num() {return 10;
}
int main() {int&& a = 10; // a是右值引用,10是纯右值std::string&& s = std::string("hello") + " world";// s是右值引用, std::string("hello") + " world"是纯右值int&& b = get_num();bool&& t = a > b;int*&& p = &a;return 0;
}

这里尤其需要注意的一点是,右值引用本身是左值。因为右值引用是具名化的,且本身也能够被取值,只是右值引用只能引用右值。

int main() {int&& a = 10;int* p = &a; //合法,说明右值引用本身是左值int&& b = a; //报错,因为右值引用本身是左值,所以不能把右值引用赋值给另一个右值引用return 0;
}

接下来,笔者将详细介绍右值引用的重要用途。




三、移动语义

移动语义是C++11及以后版本中引入的一个重要特性,它允许通过转移资源所有权的方式,将一个对象(通常为临时对象)的资源“移动”到另一个对象,而不是进行传统的复制操作。这种机制可以显著提高程序的性能和资源管理效率。如果有同学熟悉rust语言的话,移动语义也可以理解为rust的所有权转移,即移动。移动语义也是右值和右值引用的关键用途之一

1.std::move()

std::move()函数的作用简单来说,就是可以把一个左值转化为右值,在移动语义中经常需要使用。

int main() {int a = 10;int&& b = a; //报错,a是左值int&& c = std::move(a); //合法,a被转化为了右值return 0;
}

2.移动语义的使用

移动语义,简单来说就是将另一个对象的数据或内存资源”偷取“过来,目的是为了提高代码性能,避免不必要的拷贝。移动语义常用于移动构造函数和移动赋值函数中。 需要注意的是,在使用移动语义时,应当保证对象中的资源始终只有一个所有者。c++标准库中有许多容器或数据结构都提供了移动语义相关功能。

#include <iostream>  
#include <vector>  class MyString {  
private:  char* data;  size_t length;  
public:  // 普通构造函数  MyString(const char* str) : length(strlen(str)) {  data = new char[length + 1];  strcpy(data, str);  }  // 移动构造函数  MyString(MyString&& other) noexcept : data(other.data), length(other.length) {  //移交资源所有权后,将原对象资源指针置为空,即失效other.data = nullptr;  other.length = 0;  }  // 移动赋值函数  MyString& operator=(MyString&& other) noexcept { //移交资源的所有权 if (this != &other) {  delete[] data;  data = other.data;  length = other.length;  //使原对象失效other.data = nullptr;  other.length = 0;  }  return *this;  }  // 析构函数  ~MyString() {  delete[] data;  }  };  int main() {  MyString s1("Hello, world!");  MyString s2 = std::move(s1); //std::move将返回一个右值 使用移动语义初始化s2,避免不必要的内存复制  // s1现在处于有效但未定义的状态,不应该再被使用  // s2包含了原本s1的资源,可以正常使用  std::cout << s2.data << std::endl; // 输出 "Hello, world!"  return 0;  
}

在上述代码中,MyString有一个接收相同类型数据的右值引用的形参,这个构造函数又叫移动构造函数,它可以将另一个对象的数据或资源”移动“过来,这样就可以避免拷贝带来的性能损耗。当然,在上述例子中,移动之后,便把other中的指针置为空,即other已被移动,因此,一般情况下而言,被移动后的对象就无法被使用了。这也就是为什么,移动语义提倡使用右值,因为移动后的值就无法使用,而右值本身是临时的或将亡的,正好符合移动语义的场景

同样的,上述代码中的移动赋值函数也是起着同样的效果,也能将其它对象的资源进行转移。

3.移动语义的注意点:

(1)std::move()本身不移动

虽然移动语义有时需要结合std::move来将一个左值来转化为右值来进行移动,且本身也具有”移动“的意思,但是std::move本身是不会去移动资源的。真正移动资源的是程序员自己编写的那些能触发移动语义的代码(例如移动赋值函数或拷贝构造函数)。

(2)移动语义是一种规范

就像上述的MyString的例子一样,在移动构造函数中我们把other的data指针置为空。然而在具体编写代码时,尽管函数中的代码逻辑由程序员说了算,但为了确保资源的正确管理,应该遵循移动语义的规范,即在移动资源后,源对象就不再拥有该资源因此,在C++中,移动语义是一种规范或约定,它要求程序员在移动资源后,将源对象的资源指针置为无效状态(如nullptr),以确保资源的唯一所有权,并避免潜在的悬挂指针或双重释放等问题。移动后的对象就不应该再被用作拥有该资源的对象。



四、完美转发

完美转发是C++11引入的一项特性,它允许函数模板将其参数以原始的状态(包括值类别、const限定符等)传递给另一个函数。这对于泛型编程和避免不必要的拷贝或移动操作非常有用。如果你对c++的模板还不是很了解,推荐你去看看我的上一篇文章:C++进阶,一文带你迅速入门c++模板元编程!

完美转发主要解决的问题是,当需要将一个函数的参数转发给另一个函数时,通常需要保留原始参数的左右值属性如果不使用完美转发,那么为了保证能保留原始参数的属性就需要编写大量重复代码,否则参数在传递过程中可能会发生额外的拷贝或移动操作,导致性能下降或语义错误。

当然在介绍完美转发之前,需要了解一下什么是引用折叠。

1.引用折叠

引用折叠主要针对于模板函数,使用引用折叠可以让一个函数即接收左值,也能接收右值。引用折叠的规则可以归纳为以下几点:

(1)两个左值引用(&)结合:它们会折叠成一个左值引用(&)。

例如:X& & 折叠为 X&

(2)左值引用(&)和右值引用(&&)结合:它们会折叠成一个左值引用(&)。

例如:X& && 折叠为 X&
这里的右值引用作为模板参数时,可能会与左值结合,编译器会在模板形参类型前自动加&,形成& &&,然后依据规则折叠为&。

(3)两个右值引用(&&)结合:它们会折叠成一个右值引用(&&)。

例如:X&& && 折叠为 X&&

(3)非引用类型与右值引用(&&)结合:在模板参数推导中,非引用类型与右值引用结合时,会推导出右值引用类型。

例如:在模板函数template void f(T&& param);中,如果param被左值初始化,类型推导会使T成为左值的引用类型(如int&),然后T&&变为左值引用;如果param被右值初始化,T则是右值引用的类型(如int&&),此时T&&保持为右值引用。

具体可参照下表:

模板类型实际类型最终类型
T&TT&
T&T&T&
T&T&&T&
T&&TT&&
T&&T&T&
T&&T&&T&&
template<class T>
//T&& 也称为万能引用
void f(T&& arg) {//todo
}
int main() {int a = 10;f(a); // 既能接收左值f(10); // 也能接收右值return 0;
}

简单一句话来讲,就是”遇左则左,同右则右“

2.完美转发的简单实现

完美转发需要借助一个名为static_cast<T>()的函数,该函数的作用是将参数的类型转为T,并返回。如下面代码所示,我们重载了3个接收不同类型函数的fun,并且定义了一个名为my_forward
模板函数;参数为T&&(万能引用),可以接收任意的数据类型(即左值、右值都能接收)并且发生引用折叠(具体规则请看上文表格)。这里需要尤其注意的是,引用(无论左值引用右值引用)本身是左值,所以在把参数转发给fun时,需要使用static_cast<T&&>来将其再次强转为T&&,来对参数进行转发,配合引用折叠,这样就能保留参数原有的属性并转发给正确的函数处理。

void fun(int&& a) {std::cout << "rvalue" << std::endl;
}
void fun(int& a) {std::cout << "lvalue" << std::endl;
}
void fun(const int& a) {std::cout << "const lvalue" << std::endl;
}
template<class T>
void my_forward(T&& arg) {fun(static_cast<T&&>(arg));
}
int main() {int a = 10;my_forward(std::move(a)); // rvaluemy_forward(a); // lvaluemy_forward(10); // rvalueconst int& t = 90;my_forward(t); // const lvaluereturn 0;
}

3.完美转发的标准实现

事实上c++为我们提供了用于完美转发的函数forward<T>();我们只需要把上述代码中的static_cast<T&&>替换为forward<T>()即可。

void fun(int&& a) {std::cout << "rvalue" << std::endl;
}
void fun(int& a) {std::cout << "lvalue" << std::endl;
}
void fun(const int& a) {std::cout << "const lvalue" << std::endl;
}
template<class T>
void perfect_forward(T&& arg) { // 完美转发fun(std::forward<T>(arg));
}
int main() {int a = 10;const int b = 20;perfect_forward(a); // lvalueperfect_forward(b); // const valueperfect_forward(std::move(a));// rvalueperfect_forward(7); // rvaluereturn 0;
}

事实上forward<T>的原理和static_cast<T&&>差不多,这是forward<T>()的源码,可以发现其内部也使用了static_cast<T&&>

template<typename _Tp>_GLIBCXX_NODISCARDconstexpr _Tp&&forward(typename std::remove_reference<_Tp>::type& __t) noexcept{ return static_cast<_Tp&&>(__t); }



五、总结

左值引用、右值引用、移动语义和完美转发是C++11及以后版本中引入的重要特性,它们提供了更强大的资源管理和性能优化手段。左值引用和右值引用的引入使得C++能够更清晰地表达对象的生命周期和资源的所有权而移动语义则允许程序员在不需要复制资源的情况下“移动”它们,从而提高程序的性能。完美转发则是一种模板编程技术,它允许函数模板将其参数原封不动地转发给另一个函数,包括参数的值类别,在编写通用的包装函数或转发函数时非常有用。

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

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

相关文章

一文解答 | 代码签名证书怎么选

在当代软件开发中&#xff0c;代码签名证书对于确保软件的完整性、安全性及其可信度至关重要。它通过数字签名验证代码的来源和未被篡改的状态&#xff0c;向最终用户确保软件的可靠性。选择合适的代码签名证书既有利于保护软件开发商的声誉&#xff0c;也有助于建立用户对软件…

虚拟化 之三 详解 jailhouse(ARM 平台)的构建过程、配置及使用

嵌入式平台下,由于资源的限制,通常不具备通用性的 Linux 发行版,各大主流厂商都会提供自己的 Linux 发行版。这个发行版通常是基于某个 Linux 发行版构建系统来构建的,而不是全部手动构建,目前主流的 Linux 发行版构建系统是 Linux 基金会开发的 Yocto 构建系统。 基本环…

ChatGPT:自然语言处理的新纪元与OpenAI的深度融合

随着人工智能技术的蓬勃发展&#xff0c;自然语言处理&#xff08;NLP&#xff09;领域取得了显著的进步。OpenAI作为这一领域的领军者&#xff0c;以其卓越的技术实力和创新能力&#xff0c;不断推动着NLP领域向前发展。其中ChatGPT作为OpenAI的重要成果更是在全球范围内引起了…

go interface

package mainimport "fmt"// 接口 interface func main() {c : Chinese{} //创建一个中国人实例u : American{} //创建一个美国人实例greet(c) //中国人打招呼greet(u) //美国人打招呼 }// 接收具备SayHello接口能力的变量 func greet(s SayHello) {…

Vertical Layout 、Horizontal Layout 实验窗体自适应布局

实验目的 学习实验使用布局实现如下自适应界面 窗体邮件&#xff0c;布局设置为垂直布局 用同样的方法&#xff0c;添加groupbox&#xff0c;并右键设置为水平布局 拖入一个Horizontal Layout&#xff0c;然后拖入button&#xff0c;拖入 Horizontal Spacer 遇到一个问题&#…

如何将ai集成到radsystems项目中,在项目中引入ai

AI可以自动化重复性和低价值的任务&#xff0c;例如数据输入、文档处理、信息检索等&#xff0c;让员工能够专注于更具战略性和创造性的工作。通过引入AI驱动的聊天机器人或虚拟助手&#xff0c;可以提供24/7的客户支持&#xff0c;快速响应用户的问题&#xff0c;提高客户满意…

卡塔尔.巴林:海外媒体投放-宣发.发稿效果显著提高

引言 卡塔尔和巴林两国积极采取措施&#xff0c;通过海外媒体投放和宣发&#xff0c;将本国的商业新闻和相关信息传达给更广泛的受众。在这一过程中&#xff0c;卡塔尔新闻网、巴林商业新闻和摩纳哥新闻网等媒体起到了关键作用。通过投放新闻稿&#xff0c;这些国际化的媒体平…

CBoard开源数据可视化工具

CBoard开源数据可视化工具 文章目录 CBoard开源数据可视化工具介绍资源列表基础环境一、安装JDK二、安装Maven2.1、安装Maven2.2、配置Maven 三、安装Tomcat8四、安装MySQL5版本4.1、安装相关依赖4.2、二进制安装4.3、设定配置文件4.4、配置systemcatl方式启动4.5、访问MySQL数…

VMware安装ubuntu22.4虚拟机超详细图文教程

一 、下载镜像 下载地址&#xff1a;Index of /ubuntu-releases/22.04.4/ | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror 二、创建虚拟机 打开VMware点击左上角文件&#xff0c;创建新的虚拟机&#xff0c;打开后如下图&#xff1a; 下一步&#xff0c;镜像文件就是…

超市陈列艺术:不仅仅是货品摆放,更是营销策略的体现

品类管理在门店落地的最直观表现就是单品的空间陈列管理&#xff0c;通过陈列细节的差异体现出门店的商品定位与策略。此文分析入木三分&#xff0c;值得学习。 在商品陈列的空间管理领域&#xff0c;不仅要考虑整体的空间陈列&#xff0c;也要对每个商品的空间陈列位置&#…

做了2年前端,盘点前端技术栈!大佬轻喷~

前言 自己写了快两年前端&#xff0c;但是大致总结一下哈哈哈哈我觉得这个话题蛮有意思的&#xff0c;可以看看大家的技术广度&#xff0c;可以进行分享和学习以及讨论所以这里说一下我对我的前端技术&#xff0c;做一下盘点和总结因为我的开发年限有限&#xff0c;所以我觉得…

焦化行业排放平台简介

在当今社会&#xff0c;环保事业日益受到人们的关注。焦化行业作为重要的工业领域之一&#xff0c;其排放问题一直是环保工作的重点。为了有效控制焦化行业的排放&#xff0c;实施焦化行业排放平台成为了必不可少的措施。朗观视觉小编将详细探讨焦化行业排放平台的实施范围&…

【复旦邱锡鹏教授《神经网络与深度学习公开课》笔记】线性分类模型损失函数对比

本节均以二分类问题为例进行展开&#xff0c;统一定义类别标签 y ∈ { 1 , − 1 } y\in\{1,-1\} y∈{1,−1}&#xff0c;则分类正确时 y f ( x ; w ) > 0 yf(x;w)>0 yf(x;w)>0&#xff0c;且值越大越正确&#xff1b;错误时 y f ( x ; w ) < 0 yf(x;w)<0 yf(x;…

ubtun虚拟机安装

选择镜像后启动 选择第一个回车 加载完成后 &#xff0c;进入Ubuntu安装界面&#xff0c;安装语言选择English&#xff0c;完成后按一下回车&#xff1a; 此时弹出安装器可更新提示&#xff0c;下方选项选择第二个Continue without updating&#xff08;不更新&#xff0c;继续…

升级和维护老旧LabVIEW程序

在升级老旧LabVIEW程序至64位环境时&#xff0c;需要解决兼容性、性能和稳定性等问题。本文从软件升级、硬件兼容性、程序优化、故障修复等多个角度详细分析。具体包括64位迁移注意事项、修复页面跳转崩溃、解决关闭程序后残留进程的问题&#xff0c;确保程序在新环境中的平稳运…

k8s中的pod域名解析失败定位案例

问题描述 我在k8s中启动了一个Host网络模式的pod&#xff0c;这个pod的域名解析失败了。 定位步骤 敲kubectl exec -it [pod_name] -- bash进入pod后台&#xff0c;查看/etc/resolv.conf&#xff0c;发现nameserver配的有问题。这里我预期的nameserver应该使用宿主机的&…

亚马逊跟卖选品不再迷茫,适合跟卖卖家得一款选品软件工具!

对于刚开始做跟卖的卖家而言&#xff0c;刚开始最头疼的应该就是选品了吧&#xff0c;不知道跟卖什么产品&#xff0c;不是知道怎么有效的选择跟卖产品&#xff0c;所以很多卖家都会借助&#xff0c;选品软件来进行选品&#xff0c;这样不仅能提高选品的效率&#xff0c;还能帮…

人工智能的社会应用:深刻变革的新浪潮

人工智能的社会应用&#xff08;语言文本方面&#xff09; 人工智能在社会应用中的广泛运用体现在多个领域&#xff0c;特别是在语音和文本处理方面。以下是这些技术的一些扩展&#xff1a; 1. 文本翻译&#xff1a; 谷歌翻译&#xff1a;利用深度学习模型&#xff0c;支持100多…

pom学习笔记:kimi的自动化操作

1.先看结构&#xff1a; 声明&#xff1a;我是初学&#xff0c;可能有不合理的地方。 2.Base层。 我是把原来一个kimi的自动问答的代码改过来。 分析&#xff1a;其实我是新手&#xff0c;因为我用的浏览器是固定的&#xff0c;也没有打算和别人用。所以浏览器层面年的全部写…

App UI 风格展现非凡创意

App UI 风格展现非凡创意