【C++11】右值引用、移动构造、移动赋值、完美转发 的原理介绍

news2024/11/21 1:44:07

文章目录

  • 一、概念
    • 1.1 左值
    • 1.2 左值引用
    • 1.3 什么是右值?
    • 1.4 什么是右值引用?
      • 对于参数左值还是右值的不同,是被重载支持的
      • 左值引用的使用场景 和 缺陷
  • 二、移动语义
    • 2.1 移动拷贝构造
    • 2.2 移动赋值
  • 三、右值引用 与 STL
    • 3.1 移动拷贝构造 和 赋值重载
    • 3.2 插入接口
    • 3.3 完美转发、万能引用
      • 完美转发
      • 万能引用


传统的 C++ 语法中就有引用的语法,而 C++11 中新增了的右值引用语法特性,所以我们管之前的引用叫做左值引用。无论左值引用还是右值引用,都是给对象取别名


一、概念

1.1 左值

  • 左值是一个表示数据的表达式(如变量名或解引用的指针),通俗的理解就是,能取到地址的就是左值

  • 正常情况下我们可以对左值赋值,定义 const 修饰后的左值,不能给它赋值,但是可以取出它的地址;

  • 左值可以出现在赋值符号 " = " 的左边,也可以出现在赋值符号的右边;

  • 左值具有持久的状态。

// 常见的左值
int a = 0;
int b = 1;
int* p = &a;
const int c = 3;

1.2 左值引用

  • 左值引用:就是对左值的引用,相当于给左值取别名。
  • 左值引用只能引用左值,不能直接引用右值;但是 const 左值引用可以引用右值。
  • 左值引用符号 &
// 左值引用给左值取别名
int& ref1 = a;

// 左值引用给右值取别名
// int& ref2 = (a + b);			// err...(a+b)返回的是临时对象,临时对象具有常性,出现权限放大问题
const int& ref2 = (a + b);		// 加 const 就行了

1.3 什么是右值?

  • 右值也是一个数据表达式,右值是 字面常量 或者是 求值过程种创建的临时对象

  • 对于右值,不能取出地址,不能对它赋值

  • 右值的生命周期是短暂的,如:字面常量,表达式返回值,函数返回值(不是左值引用的返回值),临时变量,匿名对象…;

// 右值
a + b;
func(x,y);
10;
"abcd";

1.4 什么是右值引用?

  • 右值引用相当于给右值起别名,右值引用只能右值,除非左值 move 后,可以对其进行右值引用
  • 右值是没有地址的,但是右值引用后,这个右值引用会被存到特定的位置,且可以取到该值的地址,也就是说 右值引用值是一个左值
  • 右值引用会开辟一块空间去存右值,普通的右值引用可以修改这块空间,const 的右值引用则不可以被修改。
  • 右值引用符号 &&

move():标准库中的函数,可以将左值强制转换为右值

// 右值引用给右值取别名:
int&& ref3 = (a + b);			

// 右值引用不能给左值起别名:
//int&& ref4 = a;		// err

// 右值引用可以 给 move 后的左值取别名:
int&& ref4 = move(a);

对于参数左值还是右值的不同,是被重载支持的

//void func(const int& a)	// 这样虽说都可以使用,但是区分不了左值和右值
void func(int& a)
{
	cout << "void func(int& a)" << endl;
}

void func(int&& a)
{
	cout << "void func(int&& a)" << endl;
}

int main()
{
	int a = 0;
	int b = 1;
	func(a);
	func(a + b);
	return 0;
}
----------------------------------
输出结果:
void func(int& a)
void func(int&& a)

左值引用的使用场景 和 缺陷

左值引用可以直接减少拷贝。应用如下:

  1. 左值引用传参;
  2. 传引用返回。

左值引用解决了大多数场景的问题,但也存在一些解决不了的问题:

  1. 局部对象返回问题;
  2. 对象深拷贝问题。

C++11 前,对于带指针的容器(比如 string)会进行深拷贝,深拷贝的目的是让赋值和被赋值的对象,都正常保存并使用数据互不影响,代价比较高。而如果被拷贝对象是右值,右值本不需要留存浅拷贝就可以,但还是进行了深拷贝,是很不划算的。

ttang::string s1("hello world");

ttang::string ret1 = s1;		// 左值拷贝
ttang::string ret2 = (s1+'!');	// 右值拷贝,如果也是深拷贝,很不划算
// 实际上右值拷贝,C++11使用的是移动拷贝构造,是浅拷贝
// 可以很大程度的优化右值的拷贝效率

二、移动语义

2.1 移动拷贝构造

移动拷贝构造函数跟构造函数一样,参数需要是一个本身类型的对象,但 移动拷贝构造函数的参数是一个该类型的右值引用

所谓移动,是数据交换。移动拷贝函数创建出一个新对象,将新对象中的值都设置为 0,接下来与传进来的右值对象进行资源交换

// 移动拷贝构造
string(string&& s)
	:_str(nullptr),
	_size(0),
	_capacity(0)
{
	swap(s);
}
  • 根据函数匹配规则,如果调用拷贝构造对象的时候传的是左值,编译器会自动匹配到拷贝构造函数,如果传的是右值,那么就会匹配到移动拷贝函数。

  • 使用移动拷贝构造函数后,源对象指向资源就被交换出去,这些资源的所有权都归属到了新对象。
    因此,如果源对象是一个长期存在的对象的时候,需要谨慎使用移动拷贝构造函数。调用移动拷贝构造函数创建出 s3,s1 的资源被转移到了 s3,s1 中没有指向任何资源,所以就不能通过 s1 去寻找之前的资源。

  • 如果只是调用 move() 函数,而不对其返回值进行接收,是不会改变传入值的内容的。

std::string s1("hello");

// 拷贝
std::string s2 = s1;
// 移动(s3 里面存了“hello”,而 s1 空了)
std::string s3 = move(s1);

// 只是这样不接受其返回值,是不会改变s2的
move(s2);
std::string s4 = s2;

一些使用举例:

list<ttang::string> lt;

ttang::string s1("hello world");		
lt.push_back(s1);							// 深拷贝
lt.push_back(move(s1));						// 移动

lt.push_back(ttang::string("hello world"));	// 移动,匿名对象也是右值
lt.push_back("hello world");				// 移动

总的看来:

  • 左值引用减少拷贝,右值引用也是减少拷贝,提高效率;
  • 但是他们的角度不同,左值引用是直接减少拷贝;右值引用是间接减少拷贝,通过函数重载,识别出是左值还是右值,如果是右值,则不再深拷贝,直接移动拷贝,提高效率。

2.2 移动赋值

对于初始化:

  • 如果 string 类中只有一个移动拷贝构造函数,那么函数返回值构造临新对象的时候,那么只需要调用一次 移动拷贝构造 函数将资源转移给新对象。

对于已经初始化过的对象进行赋值:

  • 那么就会调用一次 移动拷贝构造 函数和一次 赋值重载 函数,赋值重载函数也是进行深拷贝的。

因此,为了解决赋值重载的深拷贝的问题,我们还需要再实现一个移动赋值重载函数,移动赋值重载 函数跟拷贝构造函数一样,都是 解决深拷贝的问题,都是进行转移资源

移动赋值重载函数跟赋值重载函数的定义是类似的,只是移动赋值重载函数的参数是右值引用,是为了让右值能够调用该函数,如下:

// 移动赋值
string& operator=(string&& s)
{
	swap(s);
	return *this;
}

三、右值引用 与 STL

综上推演,C++11 为很多容器都增加了 移动拷贝构造函数赋值重载函数的右值引用版本,包括 push_back 或者 insert 接口也增加了右值引用 的重载

3.1 移动拷贝构造 和 赋值重载

在这里插入图片描述
在这里插入图片描述

3.2 插入接口

在这里插入图片描述
在这里插入图片描述

上面的文档中,insert 和 push_back 可以接收 const 左值引用,也就是说这个函数既可以接收左值,也可以接受右值,那么为什么还需要多定义一个参数是右值引用的函数重载呢?

  • 因为,我们说了左值引用可以接收右值但是相应的拷贝任然是深拷贝,重载右值引用版本正是为了优化这一点。
  • 再梳理一下传入右值的流程:如果调用 vector 中 insert 时,传的参数是右值,那么就会编译器就会匹配到右值引用参数的 insert 的重载函数,因为 insert 函数内部会对该参数值进行赋值重载到 vector 内,如果是右值,那么就会调用移动拷贝构造函数,所以可以避免深拷贝的出现。
vector<string> v;
string str("hello");
v.insert(v.end(), str);	// str 是左值,深拷贝
v.insert(v.end(), string("hello~"))	// string("xx") 是匿名对象,调用右值版本
  • 因此,我们在赋值、构造、以及调用 insert、push_back、时,如果涉及深拷贝的问题,尽量传右值(匿名对象),这样可以减少深拷贝的问题。

3.3 完美转发、万能引用

完美转发

关于右值引用本身的属性,举例:

int&& a = 10;
cout << &a << endl;	// a 可取地址
a++;				// a 可修改

右值引用的 a,虽然是通过右值得到的,但 a 本身是左值!!!

进一步举例:

void insert(iterator pos, const T&& val)
{
	//...
	*pos = val;	// val 是左值,调用的是赋值重载
	++_end;
}

很明显的,在上面调用参数是右值引用的 insert 接口中存在一个问题,如果右值引用 val 去接受一个右值,那么这个 val 就会退化成一个左值。所以 *pos = val; 这一步调用的还是重载函数,不是移动拷贝构造。因此,为了保持 val 是一个右值,有一个专门的写法:

std::forward<T>(val):它可以在传参的时候保持 val 原生属性,也就是可以保持其右值属性,因此,这样可以保证 *pos = val; 调用的会是移动赋值重载函数。

这种调用 std::forward(val),使得 val 保持原生属性的过程就是 完美转发,写成如下:

void insert(iterator pos, const T&& val)
{
	//...
	*pos = std::forward<T>(val);	// 保持了 val 的右值属性!调用的就是其移动赋值函数
	++_end;
}

万能引用

万能引用 又叫 引用折叠,使用 && 进行定义或申明。万能引用定义的参数,即可以对左值引用,也可以对右值引用。

使用场景:函数模板的形参auto 声明

template<typename T> 
void f(T&& param);   //param是个万能引用

auto && var2 = var1;

以上两种场景的共同之处,在于它们都涉及类型推导。

下面这两种只是普通的右值引用:

template<typename T> 
void f(std::vector<T>&& param); // param 是右值引用

template<typename T> 
void f(const T&& param);   		// 加 const 则是右值引用

对于万能引用和右值引用的区分,不必过多纠结~ 能用就行。

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }

void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }

template<typename T>
void PerfectForward(T&& t)	// 这里面用这个右值转化的左值 t 可以,但是再转一层就会出问题
{
	Fun(forward<T>(t));		// 不加完美转发的话,会全是左值引用!!!
}

int main()
{
	int a;
	PerfectForward(a);            // 左值
	PerfectForward(std::move(a)); // 右值
	PerfectForward(10);           // 右值
	const int b = 8;
	PerfectForward(b);		      // const 左值
	PerfectForward(std::move(b)); // const 右值

	return 0;
}

🥰如果本文对你有些帮助,请给个赞或收藏,你的支持是对作者大大莫大的鼓励!!(✿◡‿◡) 欢迎评论留言~~


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

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

相关文章

Web攻防03_MySQL注入_数据请求

文章目录 PHP-MYSQL-数据请求类型1、数字型(无符号干扰)2、字符型&#xff08;有符号干扰&#xff09;3、搜索型&#xff08;有多符号干扰&#xff09;4、框架型&#xff08;有各种符号干扰&#xff09; PHP-MYSQL-数据请求方法数据请求方法GET&#xff1a;POST&#xff1a;Coo…

运筹系列86:MIP问题的建模tips

1. Either-or constraint 添加辅助变量y。 比如 Either 3 x 1 2 x 2 ≤ 18 3x_12x_2 \le 18 3x1​2x2​≤18 or x 1 4 x 2 ≤ 16 x_14x_2 \le 16 x1​4x2​≤16 可以用 3 x 1 2 x 2 ≤ 18 M y 3x_12x_2 \le 18My 3x1​2x2​≤18My x 1 4 x 2 ≤ 16 M ( 1 − y ) x_1…

【vue】el-carousel实现视频自动播放与自动切换到下一个视频功能:

文章目录 一、原因:二、实现代码:三、遇到的问题&#xff1a;【1】问题&#xff1a;el-carousel页面的视频不更新【2】问题&#xff1a;多按几次左按钮&#xff0c;其中跳过没有播放的视频没有销毁&#xff0c;造成再次自动播放时会跳页 一、原因: 由于后端无法实现将多条视频拼…

2023年:哪些Trello的替代品值得关注?

你还记得你的第一块Trello板吗&#xff1f;它可能帮助你记录了大学申请、培训目标&#xff0c;甚至是圣诞礼物这些待办事项。对于我们中的许多人来说&#xff0c;Trello 是我们尝试的第一个工作管理工具。无论是跟踪高中作业&#xff0c;组织家庭任务&#xff0c;还是可视化工作…

瑞芯微RKNN开发·yolov5

官方预训练模型转换 下载yolov5-v6.0分支源码解压到本地&#xff0c;并配置基础运行环境。下载官方预训练模型 yolov5n.ptyolov5s.ptyolov5m.pt… 进入yolov5-6.0目录下&#xff0c;新建文件夹weights&#xff0c;并将步骤2中下载的权重文件放进去。修改models/yolo.py文件 …

【C++】继承 ⑥ ( 继承中的构造函数和析构函数 | 类型兼容性原则 | 父类指针 指向 子类对象 | 使用 子类对象 为 父类对象 进行初始化 )

文章目录 一、public 公有继承 - 示例分析1、类型兼容性原则2、类型兼容性原则应用场景 二、类型兼容性原则 - 示例分析1、父类指针 指向 子类对象2、使用 子类对象 为 父类对象 进行初始化3、完整代码示例 一、public 公有继承 - 示例分析 1、类型兼容性原则 类型兼容性原则 :…

测试开发之性能篇 —— 性能测试设计

很多朋友接触性能测试是从工具开始的&#xff0c;比如流行的JMeter、Loadrunner等。熟悉一个测试工具&#xff0c;有助于对性能测试的过程、方法和机制有个直观的理解。 我们知道&#xff0c;无论是什么类型的测试&#xff0c;其目标不外乎两个&#xff0c;一是为了证明系统满…

直播带货前途渺茫了

我是卢松松&#xff0c;点点上面的头像&#xff0c;欢迎关注我哦&#xff01; 种种迹象表明电商行业和直播带货将受到冲击。直播带货前途渺茫了&#xff0c;相信很快就有政策出来了&#xff0c;针对电商这块的&#xff0c;支持实体、支持取消直播带货。 (1)目前&#xff0c;…

C++对多继承的理解

学到C时我们知道了继承但是一般都是使用单继承为主&#xff0c;单继承就是一个子类只能继承一个父类而多继承是指一个子类可以同时继承多个父类。 菱形继承 菱形继承是多继承中的一个特殊情况。当一个子类同时继承两个具有共同父类的类时&#xff0c;就会出现菱形继承问题。但…

2 spring 识别自定义实现BeanFactoryPostProcessor 的接口

如果自定义实现了BeanFactoryPostProcessor接口&#xff0c;那么想让spring识别到的话&#xff0c;有两种方式&#xff1a; 1 定义在spring的配置文件中&#xff0c;让spring自动识别 2 调用具体的addBeanFactoryPostProcessor方法 方法1 的代码实现 定义实现BeanFactoryPo…

淘宝拍立淘接口,按图搜索商品接口,图片识别接口,图片上传搜索接口,图片搜索API接口,以图搜货接口

淘宝拍立淘图片搜索接口可以通过上传或输入图片链接的方式&#xff0c;调用淘宝的图片搜索引擎&#xff0c;返回与该图片相关的所有淘宝商品。 使用该接口需要先申请淘宝开放平台的App Key和App Secret&#xff0c;获取相应的API访问权限。在调用接口时&#xff0c;需要传入商…

YOLOv5算法改进(12)— 如何去更换主干网络(1)(包括代码+添加步骤+网络结构图)

前言:Hello大家好,我是小哥谈。YOLOv5采用的主干网络是CSPDarknet53,它是Darknet53的改进版本,采用了Cross Stage Partial连接(CSP)结构,可以提高模型的效率和准确率。在学术上,为了提升YOLOv5算法模型的准确率或者鲁棒性等,已经有很多改进方案问世。更换主干网络作为…

[C++]:2初识C++(auto) + 类和对象上:

[TOC](初识C(auto) 类和对象上) 一.初始C 1.auto关键字&#xff1a;(C11) 1.作为一个变量的类型给这个类型初始化&#xff0c;auto自动识别初始化这个变量值的类型&#xff0c;为auto类型的这个变量开辟一个合适的空间。 补充&#xff1a; 1.typeid(变量名).name—>可以打…

快速入门:Spring Cache

目录 一:Spring Cache简介 二:Spring Cache常用注解 2.1:EnableCaching 2.2: Cacheable 2.3:CachePut 2.4:CacheEvict 三:Spring Cache案例 3.1:先在pom.xml中引入两个依赖 3.2:案例 3.2.1:构建数据库表 3.2.2:构建User类 3.2.3:构建Controller mapper层代码 3.…

ModuleNotFoundError: No module named ‘torch‘

目录 情况1,真的没有安装pytorch情况2(安装了与CUDA不对应的pytorch版本导致无法识别出torch) 情况1,真的没有安装pytorch 虚拟环境里面真的是没有torch,这种情况就easy job了,点击此链接直接安装与CUDA对应的pytorch版本,CTRLF直接搜索对应CUDA版本即可查找到对应的命令.按图…

【复盘】主从延迟以及 Waiting for tablemetadata lock 线上问题

背景 今晚DBA给一个大表添加索引&#xff0c;1000多W&#xff0c;正好风控系统这个时间段有查询这个表的请求&#xff0c;于是就出现了复制延迟。 这是正常下的延迟 可以看出基本都是是100毫秒以下。 Waiting for tablemetadata lock&#xff0c;并且业务跑的SQL出现锁等待…

实现Traefik工具Dashboard远程访问:搭建便捷的远程管理平台

文章目录 前言1. Docker 部署 Trfɪk2. 本地访问traefik测试3. Linux 安装cpolar4. 配置Traefik公网访问地址5. 公网远程访问Traefik6. 固定Traefik公网地址 前言 Trfɪk 是一个云原生的新型的 HTTP 反向代理、负载均衡软件&#xff0c;能轻易的部署微服务。它支持多种后端 (D…

24-数据结构-内部排序-基数排序

基数排序 基数排序&#xff0c;给关键字分成d位&#xff08;组&#xff09;&#xff0c;&#xff0c;对每一位的情况&#xff0c;可能会出现的值位r&#xff08;基数&#xff09;个&#xff0c;然后分成r个队列&#xff0c;对每个对林进行分配耗时O(n)&#xff0c;最后按照改位…

join、inner join、left join、right join、outer join的区别

内连接 inner join(等值连接)&#xff1a;只显示两表联结字段相等的行&#xff0c;(很少用到&#xff0c;最好别用)&#xff1b; 外连接 left join&#xff1a;以左表为基础,显示左表中的所有记录,不管是否与关联条件相匹配,而右表中的数据只显示与关联条件相匹配的记录,不匹配…

【C/PTA】顺序结构专项练习

本文结合PTA专项练习带领读者掌握顺序结构&#xff0c;刷题为主注释为辅&#xff0c;在代码中理解思路&#xff0c;其它不做过多叙述。 7-1 是不是太胖了 据说一个人的标准体重应该是其身高&#xff08;单位&#xff1a;厘米&#xff09;减去100、再乘以0.9所得到的公斤数。已…