【C++】右值引用与完美转发

目录

一、右值引用:

1、左值与右值:

2、左值引用和右值引用:

二、右值引用的使用场景:

1、左值引用的使用场景:

2、右值引用的使用场景:

移动构造

移动赋值

三、完美转发:

1、万能引用:

2、实际使用:


一、右值引用:

1、左值与右值:

在了解右值引用前,要先了解什么是左值引用(其实在之前已经使用过很多回了),那么要了解什么是左值引用,就先要了解什么是左值什么是右值

在等号左边的值叫左值吗?在等号右边的值叫右值吗? 

显然定义不会这么简单的,但是左值可以出现赋值符号的左边,右值不能出现在赋值符号左边,也就是说在等号左边的值一定不是右值,在等号右边的可以是左值或右值

左值:

能够进行取地址的操作,也可以被修改

如下的a,b,c都是左值

int main()
{int a = 10;const int b = 10;int* c = new int(0);return 0;
}

右值:

不能够进行取地址的操作,一般不能够被修改

如下,10,x,fmin(x,y)的返回值就是右值

int main()
{double x = 1.1, y = 2.2;//如下就是右值10;x + y;fmin(x, y);return 0;
}

理解:

1、右值的本质是一个临时变量或者常量值
2、这些临时变量是没有被实际存储起来的,所以无法对右值取地址        
3、像上述的fmin的返回值,其实际上就是一份临时拷贝,所以算作右值

2、左值引用和右值引用:

什么是左值引用:

左值引用就是给左值取别名

int main()
{int a = 10;const int b = 10;int* c = new int(0);//这就是左值引用int& pa = a;const int& pb = b;int*& pc = c;return 0;
}

什么是右值引用:

右值引用就是给右值取别名

int main()
{double x = 1.1, y = 2.2;//如下就是右值10;x + y;fmin(x, y);//这个就是右值引用int&& p1 = 10;int&& p2 = x + y;int&& p3 = fmin(x, y);return 0;
}

这里在给右值取别名后,右值会被存储到特定的位置,此时就能够取到该位置的地址了

左值引用可以引用右值吗 ----- 可以

但是,左值引用不能直接引用右值,因为右值不能够被修改,左值可以修改,如果直接引用的话权限会存在放大问题,所以如果想要左值引用右值就需要加上const修饰

像我们之前在函数参数传参的时候经常写const T& x,这就是保证既能够传左值,又能够传右值

template<class T>
void func(const T& val)
{cout << val << endl;
}
int main()
{string s("111");func(s);       //s为左值func("222"); //"222"为右值return 0;
}

右值引用可以引用左值吗 ----- 可以

但是,右值引用也不能直接引用左值,如果想要引用左值,就需要加上move后的左值

int main()
{int a = 10;//右值引用给左值取别名int&& pa = move(a);return 0;
}

为什么加上move后才能让右值引用来引用左值呢?

我们首先要知道,左值引用或者右引用都是在给资源取别名,对于左值引用,就是直接指向原本的数据,对于右值引用,我们知道原本是没有空间资源的,那么右值引用引用右值就是首先开辟一块空间,然后将常量或者临时变量转移到开辟好的地方,然后在指向该地方

所以右值引用的本质是对右值进行资源的转移

此时就有空间资源了,此时就能够取地址了,并且能够对其进行修改了

对于常量,临时变量,表达式的结果这些右值,编译器在右值引用的时候会直接将这些右值进行转移资源,但是对于左值,编译器不敢直接转移,这个时候编译器就为用户提供了一个函数move,当进行move左值的时候,就能够让右值引用 引用左值了

二、右值引用的使用场景:

1、左值引用的使用场景:

左值引用既能够引用左值,又能够引用右值,但是还是存在短板,所以在C++11里面,引入了右值引用来弥补左值引用的短板

在左值引用中:

1、左值引用做参数,防止传参是的拷贝
2、左值引用做返回值,防止返回时对返回对象进行拷贝

首先,写一个自己的string类,在里面写上部分cout来方便打印观察

namespace ppr
{class string{public:typedef char* iterator;iterator begin(){return _str;//返回字符串中第一个字符的地址}iterator end(){return _str + _size;//返回字符串中最后一个字符的后一个字符的地址}//构造函数string(const char* str = ""){_size = strlen(str);_capacity = _size;_str = new char[_capacity + 1];strcpy(_str, str);}//交换两个对象的数据void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}//拷贝构造函数(现代写法)string(const string& s):_str(nullptr),_size(0),_capacity(0){cout << "string(const string& s) -- 深拷贝" << endl;string tmp;swap(tmp);}//移动构造函数(现代写法)string(string&& s):_str(nullptr), _size(0), _capacity(0){cout << "string(string&& s) -- 移动构造" << endl;swap(s);}//赋值运算符重载(现代写法)string& operator=(const string& s){cout << "string& operator=(const string& s) -- 深拷贝" << endl;string tmp;swap(tmp);return *this;}//析构函数~string(){delete[] _str;_str = nullptr;_size = 0;_capacity = 0;}//[]运算符重载char& operator[](size_t i){assert(i < _size);return _str[i];}//改变容量,大小不变void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];strncpy(tmp, _str, _size + 1);delete[] _str;_str = tmp;_capacity = n;}}//尾插字符void push_back(char ch){if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size] = ch;_str[_size + 1] = '\0';_size++;}//+=运算符重载string operator+=(char ch){push_back(ch);string tmp(*this);return tmp;}//返回C类型的字符串const char* c_str()const{return _str;}private:char* _str;size_t _size;size_t _capacity;};
}

接着看看左值引用的使用场景

//首先,看看左值引用的使用场景
//值传参
void func1(ppr::string s)
{cout << "void func1(ppr::string s)" << endl;
}
//左值引用传参
void func2(const ppr::string& s)
{cout << "void func2(const ppr::string& s)" << endl;
}int main()
{ppr::string ret1("1111111111111");func1(ret1);//这里采用深拷贝func2(ret1);ret1 += '0';//里面return *this 的时候,也会进行拷贝构造return 0;
}

其中,在func1的时候,传值传参会进行一次拷贝构造,在+=那里,返回* this的时候,也会进行一次拷贝构造这样的话会看到两次深拷贝

左值引用短板:

左值引用能够避免不必要的拷贝构造,但是并不能完全避免

左值引用做参数的时候,能够完全避免传参时的拷贝
左值引用做返回值的时候,不能完全避免拷贝

比如如果返回的是一个局部变量,在返回的时候局部变量被销毁了,此时如果使用左值引用进行返回,就会返回的野指针,此时就不能够用左值引用返回,需要老老实实地值拷贝

如下会进行两次拷贝操作,然后将ret返回

如果在新一点的编译器会进行优化,只需进行一次拷贝操作

如果是引用传参,此时局部变量的局部空间,在出了函数作用域之后就会被释放,此时就会出问题

所以,C++11为了解决这类问题,提出了右值引用来解决这种场景

2、右值引用的使用场景:

右值分为 纯右值将亡值

纯右值:内置类型的右值
将亡值:自定义类型的右值

右值引用和移动语句解决上述问题的方式就是,给当前模拟实现的string类增加移动构造方法

移动构造

移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己

如果没加上述的移动拷贝,就会出现深拷贝

如果加上移动构造,那么就会走移动构造函数,这样就能更加减少拷贝

移动构造的本质就是将参数的右值窃取过来,占为己有,这样它就不用再深度拷贝了,所以叫做移动构造

移动构造和拷贝构造的区别:

1、在没有增加移动构造之前,由于拷贝构造采用的是const左值引用接收参数,因此无论拷贝构造对象时传入的是左值还是右值,都会调用拷贝构造函数
2、增加移动构造之后,由于移动构造采用的是右值引用接收参数,因此如果拷贝构造对象时传入的是右值,那么就会调用移动构造函数
3、string的拷贝构造函数做的是深拷贝,而移动构造函数中只需要调用swap函数进行资源的转移,因此调用移动构造的代价比调用拷贝构造的代价小

左值引用:直接引用对象以减少拷贝

右值引用:间接减少拷贝,将临时资源等将亡值的资源通过 移动构造 进行转移,减少拷贝

移动赋值

移动赋值是一个赋值运算符重载函数,该函数的参数是右值引用类型的,移动赋值也是将传入右值的资源窃取过来,占为己有,这样就避免了深拷贝,之所以它叫移动赋值,就是窃取别人的资源来赋值给自己的意思

// 赋值重载
string& operator=(const string& s) 
{cout << "string& operator=(string s) -- 深拷贝" << endl; string tmp(s); swap(tmp); return *this;
}
//移动赋值
string& operator=(string&& s) 
{cout << "string& operator=(string && s) -- 移动拷贝" << endl; swap(s); return *this;
}

string& operator=(const string& s) 和string& operator=(string&& s) 的区别:

1、在没有string& operator=(string&& s) 的时候,如果进行=操作,那么无论是左值还是右值传参都会调用string& operator=(const string& s) 这个函数

2、在增加移动赋值后,如果是左值就调用原来的函数,如果是右值就调用新加的移动赋值函数

3、移动赋值函数是通过swap函数进行资源的交换,而原来的operator=是通过深拷贝进行,因此,移动赋值的代价比原来的要小

三、完美转发:

1、万能引用:

template<class T>
void PerfectForward(T&& t)
{//...
}

这里函数中的参数并不是右值引用,如果传的模板是左值,这里的参数就是左值引用,相反如果传的模板是右值,那么这里的参数就是右值引用

void func(int& a)
{cout << "左值引用" << endl;
}
void func(const int& a)
{cout << "const 左值引用" << endl;
}
void func(int&& a)
{cout << "右值引用" << endl;
}
void func(const int&& a)
{cout << "const 右值引用" << endl;
}
template<class T>
void perfectForward(T&& val)
{func(val);
}int main()
{int a = 10;perfectForward(a); //左值const int b = 10;  //const 左值perfectForward(b);perfectForward(move(a)); // 右值perfectForward(move(b)); //const 右值return 0;
}

如上,这就是通过func函数重载,来观察编译器会怎样进行函数调用

如上,这是运行结果,为什么会这样呢?难道是编译器做的不对吗,在实际调用中,4个函数没有一个是进入了右值引用,均匹配的是左值引用版本,这是为什么呢?

当对右值进行引用后,会导致右值被存储到特定的位置,此时就能够对这个引用后的右值进行取地址了,这样的话,这个右值就模版被识别成左值了

也就是说,在右值引用过一次后,会导致右值变成左值,但是如果想要继续保证其右值的属性,此时就需要用到完美转发

如上,在对右值引用进行传参的时候,在前面加上forward<T>,这样经过完美转发后,调用PerfectForward函数时传入的是右值就会匹配到右值引用版本的Func函数,传入的是const右值就会匹配到const右值引用版本的Func函数

forward是一个模板函数,需要指定模板参数类型T,确保能正确推导并传递

2、实际使用:

首先实现一个建议的list,在其中实现左值引用的push_back和insert函数

namespace ppr
{template<class T>struct ListNode{T _data;ListNode* _next = nullptr;ListNode* _prev = nullptr;};template<class T>class list{typedef ListNode<T> node;public://构造函数list(){_head = new node;_head->_next = _head;_head->_prev = _head;}//左值引用版本的push_backvoid push_back(const T& x){insert(_head, x);}//右值引用版本的push_backvoid push_back(T&& x){insert(_head, x);}//左值引用版本的insertvoid insert(node* pos, const T& x){node* prev = pos->_prev;node* newnode = new node;newnode->_data = x;prev->_next = newnode;newnode->_prev = prev;newnode->_next = pos;pos->_prev = newnode;}//右值引用版本的insertvoid insert(node* pos, T&& x){node* prev = pos->_prev;node* newnode = new node;newnode->_data = x;prev->_next = newnode;newnode->_prev = prev;newnode->_next = pos;pos->_prev = newnode;}private:node* _head; //指向链表头结点的指针};
}

接着进行左值和右值的push_back版本的调用

int main()
{ppr::list<ppr::string> lt;ppr::string s1("111111111111111");//左值的push_backlt.push_back(s1);cout << endl << endl;ppr::string s2("111111111111111");//右值的push_backlt.push_back(move(s2));cout << endl << endl;lt.push_back("22222222222222222");//右值的push_backreturn 0;
}

但是会发现全部都是深拷贝,这和上述右值被引用后,就可以取地址了,就变成左值了,所以为了避免这种情况,就需要在右值版本的push_back和insert加上完美转发,让右值能够保存右值属性

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

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

相关文章

wx201基于ssm+vue+uniapp的购物系统设计与实现小程序

开发语言&#xff1a;Java框架&#xff1a;ssmuniappJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;M…

Mac 常用命令

一、文件操作(必知必会)​ ​1. 快速导航 cd ~/Documents # 进入文档目录 cd .. # 返回上级目录 pwd # 显示当前路径 2. ​文件管理 touch new_file.txt # 创建空文件 mkdir -p project/{src,docs} # 递归创建目录 cp …

Nginx RTMP 处理模块 (ngx_rtmp_handler.c) 详细分析

ngx_rtmp_handler 是 Nginx RTMP 模块中的核心处理部分&#xff0c;主要负责处理 RTMP 流会话中的数据接收、发送、ping 操作以及分块大小的设置等。 1. 全局变量 ngx_rtmp_naccepted: 记录接受的 RTMP 连接数。 ngx_rtmp_bw_out 和 ngx_rtmp_bw_in: 分别表示输出带宽和输入带…

(二)万字长文解析:deepResearch如何用更长的思考时间换取更高质量的回复?各家产品对比深度详解

DeepResearch的研究背景 业务背景&#xff1a;用更长的等待时间&#xff0c;换取更高质量、更具实用性的结果 当前AI技术发展正经历从“即时响应”到“深度思考”的范式转变。用户对延迟的容忍度显著提升&#xff0c;从传统200ms的交互响应放宽至数秒甚至数分钟&#xff0c;以…

综述速读|086.04.24.Retrieval-Augmented Generation for AI-Generated Content A Survey

论文题目&#xff1a;Retrieval-Augmented Generation for AI-Generated Content: A Survey 论文地址&#xff1a;https://arxiv.org/abs/2402.19473 bib引用&#xff1a; misc{zhao2024retrievalaugmentedgenerationaigeneratedcontent,title{Retrieval-Augmented Generation…

Spring Cache:简化缓存管理的抽象框架

Spring Cache Spring Cache是Spring框架提供的缓存抽象层&#xff0c;通过注解和自动化配置&#xff0c;简化应用中对缓存的操作&#xff0c;支持多种缓存实现&#xff08;如Redis、Ehcache、Caffeine&#xff09;。 1. 核心特性 声明式缓存&#xff1a;通过注解&#xff08;…

求矩阵某列的和

设计函数sum_column( int A[E1(n)][E2(n)], int j )&#xff0c;E1(n)和E2(n)分别为用宏定义的行数和列数&#xff0c;j为列号。在该函数中&#xff0c;设计指针ptr&A[0][j]&#xff0c;通过*ptr及ptrptrE2(n)访问第j列元素&#xff0c;从而求得第j列元素的和。在主函数中定…

IM腾讯Trtc与vod云点播:实现合流录制并上传,根据参数返回视频地址

全文目录,一步到位 1.前言简介1.1 专栏传送门1.1.1 文档传送门 2. java基础使用2.1 准备工作2.1.1 云控制台获取(密钥和密钥secret)2.1.2 找到trtc控制台2.1.3 vod云点播控制台 2.2 使用准备的数据进行操作2.2.0 引入依赖2.2.1 创建TrtcUtils工具类2.2.2 TrtcReqDTO 录制请求dt…

蓝桥杯 数三角

问题描述 小明在二维坐标系中放置了 n 个点&#xff0c;他想从中选出一个包含三个点的子集&#xff0c;使得这三个点能够组成一个三角形。 由于这样的方案太多了&#xff0c;他决定只选择那些可以组成等腰三角形的方案。 请帮他计算出一共有多少种选法可以组成等腰三角形。 …

【Kafka】从理论到实践的深度解析

在当今数字化转型的时代&#xff0c;企业面临着数据量呈指数级增长、业务系统愈发复杂的挑战。在这样的背景下&#xff0c;高效的数据传输与处理技术成为了关键。Kafka&#xff0c;作为一款分布式消息队列系统&#xff0c;凭借其卓越的性能和丰富的特性&#xff0c;在众多企业的…

Linux课程学习一

一.fopen与fclose函数 linux中fopen函数直接用man fopen 去查看 函数原型 FILE * fopen(constchar *path , cost char *mode) /* * description : 打开一个文件 * param ‐ path : 指定文件路径,如&#xff1a;"./test.txt"&#xff0c;也可以直接由文件名 * param …

【区块链安全 | 第十篇】智能合约概述

部分内容与前文互补。 文章目录 一个简单的智能合约子货币&#xff08;Subcurrency&#xff09;示例区块链基础交易区块预编译合约 一个简单的智能合约 我们从一个基础示例开始&#xff0c;该示例用于设置变量的值&#xff0c;并允许其他合约访问它。 // SPDX-License-Identi…

XML标签格式转换为YOLO TXT格式

针对的是多边形&#xff08;<polygon>&#xff09;来描述对象的边界&#xff0c;而不是传统的矩形框&#xff08;<bndbox>&#xff09; import xml.etree.ElementTree as ET import os from pathlib import Path# 解析VOC格式的XML文件&#xff0c;提取目标框的标…

大唐杯02 DTM.PX4.016

01 5G关键技术概述 回传压力大&#xff1a;核心网向基站回传压力大 02 5G关键技术介绍01

CSS3学习教程,从入门到精通, CSS3 盒子模型的详细语法知识点及案例代码(23)

CSS3 盒子模型的详细语法知识点及案例代码 CSS3 盒子模型完整指南 一、盒子模型基础 每个 HTML 元素都被视为一个矩形盒子&#xff0c;由以下部分组成&#xff1a; 内容区 (Content)内边距 (Padding)边框 (Border)外边距 (Margin) 二、语法知识点详解 1. 盒子的宽和高 sel…

《Linux运维实战:Ubuntu 22.04修改root用户默认名并禁止登录》

总结&#xff1a;整理不易&#xff0c;如果对你有帮助&#xff0c;可否点赞关注一下&#xff1f; 更多详细内容请参考&#xff1a;Linux运维实战总结 一、背景信息 由于安全方面的考虑&#xff0c;先要求Ubuntu 22.04系统重的root用户禁止登录&#xff0c;并修改用户名root为ad…

docker-compose自定义网络,解决docker-compose网段路由冲突

问题排查 先route一波查看一下路由表 容器路由19和堡垒机路由冲突 解决方案 更改docker网段更改docker生成容器的网段 > 基本操作 docker network ls &#xff1a;查看docker网络列表 docker network inspect <network id/name>&#xff1a;查看某个docker网络详情…

前端 - ts - - declare声明类型

在使用typeScript的项目中 需要声明属性类型 单独的局部属性 可以直接在当前文件中声明 全局属性需要在项目根目录下新建.d.ts文件 vite会自动识别.d.ts类型文件 在该文件中使用declare声明类型有三种写法 1、在某种类型的文件中声明 2、声明window上的属性类型 3、全局声明…

[Mac]利用Hexo+Github Pages搭建个人博客

由于我这台Mac基本没啥环境&#xff0c;因此需要从零开始配置&#xff0c;供各位参考。 注意⚠️&#xff1a;MacBook (M4)使用/bin/zsh作为默认Shell&#xff0c;其对应的配置文件为~/.zshrc 参考文档&#xff1a; HEXO系列教程 | 使用GitHub部署静态博客HEXO | 小白向教程 文…

运维面试题(十一)

1.如果一个硬盘 IO 时阻塞了&#xff0c;会发生什么情况&#xff1f; 进程/线程挂起&#xff1a;发起I/O操作的进程或线程会被操作系统置为阻塞状态&#xff08;等待状态&#xff09;&#xff0c;直到I/O完成。CPU资源释放&#xff1a;阻塞的线程会让出CPU&#xff0c;操作系统…