【C++11】:右值引用移动语义完美转发

目录

  • 前言
  • 一,左值引用和右值引用
  • 二,左值引用与右值引用比较
  • 三,探索引用的底层
  • 四,右值引用使用场景和意义
    • 4.1 解决返回值问题
    • 4.2 STL容器插入接口的改变
  • 五,移动语义
  • 六,完美转发
    • 6.1 模板中的&& 万能引用
    • 6.2 forward 完美转发在传参的过程中保留对象原生类型属性

前言

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

一,左值引用和右值引用

1.什么是左值?什么是左值引用?

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址 + 一般情况可以对它赋值,左值可以出现赋值符号的左边和右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址左值引用就是给左值的引用,给左值取别名

int main()
{// 以下的p、b、c、*p都是左值//左值:可以取地址int* p = new int(0);int b = 1;const int c = 2;*p = 10;string s("11111111");s[0];cout << &c << endl;cout << &s[0] << endl;cout << &s << endl;return 0;
}

2.什么是右值?什么是右值引用?

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边右值不能取地址右值引用就是对右值的引用,给右值取别名

右值又可以分为纯右值和将亡值
纯右值内置类型右值
将亡值类类型的右值,如匿名对象,类型转换过程中产生的临时对象。

int main()
{//右值:不能取地址double x = 1.1, y = 2.2;// 以下几个都是常见的右值:常量,临时对象,匿名对象10;x + y;fmin(x, y);string("2222222");//err//cout << &10 << endl;//cout << &(x + y) << endl;//cout << &(fmin(x + y));return 0;
}

二,左值引用与右值引用比较

左值引用总结
1.左值引用只能引用左值,不能引用右值
2.但是const左值引用既可引用左值,也可引用右值

代码示例1

int main()
{// 以下的p、b、c、*p都是左值int* p = new int(0);int b = 1;const int c = 2;*p = 10;string s("11111111");s[0];//左值引用给左值取别名int& r1 = b;int*& r2 = p;int& r3 = *p;string& r4 = s;//左值引用引用给右值取别名const int& rx1 = 10;const double& rx2 = x + y;const double& rx3 = fmin(x, y);const string&& rx4 = string("2222222");return 0;
}

代码示例2
比如在经常使用的容器中其实也有左值右值的身影

	//void push_back(const T& x) //这里加const -> 既可以传左值,也可以传右值string s1("3333333");vector<string> v;v.push_back(s1); //有名对象->传左值v.push_back(string("3333333")); //匿名对象->传右值v.push_back("3333333"); //单参数构造函数支持隐式类型转换,中间会产生临时变量

右值引用总结
1.右值引用只能右值,不能引用左值
2.但是右值引用可以move以后的左值

代码示例

int main()
{double x = 1.1, y = 2.2;// 以下几个都是常见的右值:常量,临时对象,匿名对象10;x + y;fmin(x, y);string("2222222");//右值引用给右值取别名int&& rr1 = 10;double&& rr2 = x + y;double&& rr3 = fmin(x, y);//右值引用引用给左值取别名int&& rrx1 = move(b);int*&& rrx2 = move(p);int&& rrx3 = move(*p);string&& rrx4 = move(s);return 0;
}

三,探索引用的底层

在语法层面有左值引用和右值引用的概念,那在底层它们的本质是什么呢?

int main()
{//当到汇编层时,就没有左值引用右值引用的概念了,只有指针int x = 0;int& r1 = x;int&& rr1 = x + 10;//move的本质:就是强制类型转换,只是为了通过语法层的检验而已//string&& rrx5 = (string&&)s;return 0;
}

转到反汇编查看
在这里插入图片描述

结论:到了汇编层时,就没有引用的概念了,它们的本质都是指针。而move的本质,其实就是强制类型转换而已,只是为了通过语法层的检查

四,右值引用使用场景和意义

4.1 解决返回值问题

引用的意义:减少拷贝,提高效率。
左值引用解决的场景:引用传参/引用传返回值
左值引用没有彻底解决的场景:传返回值

下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!接下来我们要用到以前自己模拟实现的 string 类来验证

下面的验证都要用 VS2019 才能观察到!!

namespace bit
{class string{public:typedef char* iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}typedef const char* const_iterator;const_iterator begin()const{return _str;}const_iterator end()const{return _str + _size;}string(const char* str = ""):_size(strlen(str)), _capacity(_size){cout << "string(char* str)" << endl;_str = new char[_capacity + 1];strcpy(_str, str);}// s1.swap(s2)void swap(string& s){::swap(_str, s._str);::swap(_size, s._size);::swap(_capacity, s._capacity);}// 拷贝构造string(const string& s):_str(nullptr){cout << "string(const string& s) -- 深拷贝" << endl;reserve(s._capacity);for (auto ch : s)push_back(ch);}// 赋值重载string& operator=(const string& s){cout << "string& operator=(string s) -- 深拷贝" << endl;if (this != &s){_str[0] = '\0';_size = 0;reserve(s._capacity);for (auto ch : s)push_back(ch);}return *this;}~string(){delete[] _str;_str = nullptr;}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];if(_str){strcpy(tmp, _str);delete[] _str;}_str = tmp;_capacity = n;}}void push_back(char ch){if (_size >= _capacity){size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}_str[_size] = ch;++_size;_str[_size] = '\0';}string& operator+=(char ch){push_back(ch);return *this;}const char* c_str() const{return _str;}private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0; // 不包含最后做标识的\0};string to_string(int value){bool flag = true;if (value < 0){flag = false;value = 0 - value;}bit::string str;while (value > 0){int x = value % 10;value /= 10;str += ('0' + x);}if (flag == false){str += '-';}std::reverse(str.begin(), str.end());return str;}
}

左值引用的短板

但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回例如:bit::string to_string(int value)函数中可以看到,这里只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。

图解如下

在这里插入图片描述

右值引用解决上述问题

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

//移动构造
//临时创建的对象,用完就要消亡了
//深拷贝的类,移动构造才有意义
string(string&& s)
{cout << "string(string&& s) -- 移动拷贝" << endl;swap(s); // 直接转移资源
}int main()
{bit::string ret2 = bit::to_string(1234);return 0;
}

再运行上面bit::to_string的两个调用,我们会发现,这里没有调用深拷贝的拷贝构造,而是调用了移动构造,移动构造中没有新开空间,拷贝数据,所以效率提高了

图解如下

在这里插入图片描述
不仅仅有移动构造,还有移动赋值

在bit::string类中增加移动赋值函数,再去调用bit::to_string(1234),不过这次是将bit::to_string(1234)返回的右值对象赋值给ret1对象,这时调用的是移动赋值。

//移动赋值
string& operator=(string&& s)
{cout << "string& operator=(string&& s) -- 移动赋值" << endl;swap(s);return *this;
}int main()
{bit::string ret1;ret1 = bit::to_string(1234);return 0;
}

图解如下

在这里插入图片描述

总结

从此以后,类似下面这种传值返回的场景随便用,因为浅拷贝的类没什么代价,深拷贝的类会走移动拷贝和移动赋值

T func()
{T ret;//……return ret;
}

4.2 STL容器插入接口的改变

int main()
{list<bit::string> lt;bit::string s1("111111111111111111");// 这里调用的是拷贝构造lt.push_back(s1);// 下面调用都是移动构造lt.push_back("22222222222222222222222");lt.push_back(std::move(s1));return 0;
}

在这里插入图片描述

五,移动语义

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义

右值在传递过程中的退化。要验证这句话,接下来我们还是要用到以前自己模拟实现的 list 类,和 string类 来验证

下面的是在 VS2022 下的运行结果

namespace bit
{template <class T>struct ListNode{ListNode<T>* _next;ListNode<T>* _prev;T _data;ListNode(const T& data = T()):_next(nullptr),_prev(nullptr),_data(data){}ListNode(T&& data):_next(nullptr), _prev(nullptr), _data(data){}};template <class T,class Ref,class Ptr>struct ListIterator{typedef ListNode<T> Node;typedef ListIterator<T, Ref, Ptr> Self;//名字变得简短Node* _node;//定义一个节点指针ListIterator(Node* node):_node(node){}Self& operator++(){_node = _node->_next;return *this;}Self& operator--(){_node = _node->_prev;return *this;}//后置:返回之前的值Self operator++(int){Self tmp(*this);_node = _node->_next;return tmp;}Self operator--(int){Self tmp(*this);_node = _node->_prev;return tmp;}Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}bool operator!=(const Self& it){return _node != it._node;}bool operator==(const Self& it){return _node == it._node;}};template <class T>class list{typedef ListNode<T> Node;public:typedef ListIterator<T, T&, T*> iterator;typedef ListIterator<T, const T&, const T*> const_iterator;iterator begin(){//iterator it(_head->_next);//return it;return iterator(_head->_next);//使用匿名对象}iterator end(){return iterator(_head);}const_iterator begin()const{return const_iterator(_head->_next);}const_iterator end()const{return const_iterator(_head);}//空初始化,申请哨兵位头节点void empty_init(){_head = new Node();_head->_next = _head;_head->_prev = _head;}list(){empty_init();}list(const list<T>& lt){empty_init();//注意:使用范围for时加上const和&for (const auto& e : lt){push_back(e);}}//赋值拷贝//lt1 = lt3list<T>& operator=( list<T> lt){swap(_head, lt._head);return *this;}//析构:销毁整个链表~list(){clear();delete _head;_head = nullptr;}//清空当前数据 留头节点,其余节点释放void clear(){auto it = begin();while (it != end()){//返回删除节点的下一个节点的迭代器it = erase(it);}}//尾插:end的下一个位置void push_back(const T& x){insert(end(), x);}//右值引用版本void push_back(T&& x){insert(end(), x);}iterator insert(iterator pos, const T& x){Node* cur = pos._node;//找到当前节点Node* newnode = new Node(x);//申请节点Node* prev = cur->_prev;//找到前一个节点//prev newnode cur 进行链接newnode->_next = cur;cur->_prev = newnode;prev->_next = newnode;newnode->_prev = prev;return iterator(newnode);}//右值引用版本iterator insert(iterator pos, T&& x){Node* cur = pos._node;//找到当前节点Node* newnode = new Node(x);//申请节点Node* prev = cur->_prev;//找到前一个节点//prev newnode cur 进行链接newnode->_next = cur;cur->_prev = newnode;prev->_next = newnode;newnode->_prev = prev;return iterator(newnode);}iterator erase(iterator pos){assert(pos != end());//防止删除头节点Node* cur = pos._node;//找到当前节点Node* prev = cur->_prev;//找到前一个节点Node* next = cur->_next;//找到后一个节点prev->_next = next;next->_prev = prev;delete cur;return iterator(next);}private:Node* _head;};
}

假设插入以下值,观察运行结果

int main()
{bit::list<bit::string> lt;bit::string s1("111111111111");lt.push_back(s1);lt.push_back(bit::string("222222222"));lt.push_back("222222222");lt.push_back(move(s1));return 0;
}

运行结果

在这里插入图片描述

疑问:在 list类中,我们已经实现了右值引用版本的插入函数和构造函数,在string类中也有移动构造,为什么传递右值,结果还是调用的深拷贝呢

解答:因为右值引用本身的属性是左值, 只有是左值,才能转移它的资源(才能在string类中swap转移资源)

在这里插入图片描述

也就是说,我们在main函数中push_back右值,在参数传递的过程中,push_back函数虽然匹配的是右值引用的那个,但是右值引用的那个参数本身是左值属性的,所以它进行下一步传递时又会去匹配那个左值引用的函数了,这就是所说的右值在传递过程中的退化

使用move再进行一层类型转换,把过程中的左值再转换成右值,就可以解决问题

在这里插入图片描述

运行结果

在这里插入图片描述

六,完美转发

6.1 模板中的&& 万能引用

(1) 下面代码的模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值

(2) 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发

template<typename T>
void PerfectForward(T&& t)
{Fun(t);
}

6.2 forward 完美转发在传参的过程中保留对象原生类型属性

在下面的示例中,准备了各种形式的左值和右值,但是我们要在传参的过程中保留对象的原来的属性,就要加上forward

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)
{Fun(forward<T>(t));
}int main()
{PerfectForward(10);           // 右值int a;PerfectForward(a);            // 左值PerfectForward(std::move(a)); // 右值const int b = 8;PerfectForward(b);             // const 左值PerfectForward(std::move(b));  // const 右值return 0;
}

运行结果
在这里插入图片描述

到这里,我们可以进行一下简单的总结

1.在传递右值的过程中,如果我们要保持该对象原来的属性,可以使用 move 直接强制,也可以使用 forward 进行完美转发

2.move和forward的区别
move用于我们确定知道是一个右值引用
forward用于在模板中,不知道是左值还是右值引用时

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

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

相关文章

产品经理如何快速掌握大模型技术,享受AI红利?

前言 随着人工智能&#xff08;AI&#xff09;技术的快速发展&#xff0c;AI产品经理的角色变得越来越重要。尽管AI产品经理并不是一个新鲜的概念&#xff0c;但随着AI技术的迭代升级&#xff0c;这一角色的重要性得到了显著提升。 AI产品经理的演变 早期的AI产品可能并不会…

网络原理的TCP/IP

TCP/IP协议 1)应用层 应用层和应用程序直接相关,与程序员息息相关的一层协议,应用层协议,里面描述的内容,就是写的程序,通过网络具体按照啥样的方式来进行传输,不同的应用程序,就可以用不同的应用层协议,在实际开发的过程中,需要程序员自制应用层协议 应用层协议本质上就是对…

python: 多进程实例

1. 实例一 主进程跟子进程的通过两个队列实现全双工通信&#xff1b;如有需要主进程会提示窗口输入信息传输给子进程&#xff1b;如果子进程收到主进程的消息&#xff0c;会弹窗提示收到的消息&#xff1b;子进程弹窗提示进程即将结束&#xff1b; 详细代码如下 # -*- coding…

独立站+TikTok达人:自主营销与创意内容的完美结合

在全球电商市场迅猛发展的今天&#xff0c;独立站和TikTok达人的结合正在创造一种全新的电商营销模式。独立站作为电商平台&#xff0c;其自主性和灵活性为商家提供了广阔的发展空间&#xff1b;而TikTok达人凭借其独特的内容创作能力和庞大的粉丝基础&#xff0c;成为推动销售…

OpenStack;异构算力网络架构;算力服务与交易技术;服务编排与调度技术

目录 OpenStack 一、OpenStack概述 二、OpenStack的主要组件及功能 三、OpenStack的架构 四、OpenStack的应用场景 异构算力网络架构 算力服务与交易技术 服务编排与调度技术 OpenStack 是一个开源的云计算管理平台项目,由NASA(美国国家航空航天局)和Rackspace合作…

「AI绘画Stable Diffusion 零基础入门 」AI 绘画SD原理与工具介绍,万字详解新手入门必看!

大家好&#xff0c;我是设计师阿威 AI 绘画原理 想要入门 AI 绘画&#xff0c;首先需要了解它的原理是什么样的。 其实很早就已经有人基于深度学习模型展开了对图像生成的研究了&#xff0c;但在那时&#xff0c;生成的图像分辨率和内容都非常抽象。 直到近两年&#xff0c…

C++必修:STL之vector的模拟实现

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ &#x1f388;&#x1f388;养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; 所属专栏&#xff1a;C学习 贝蒂的主页&#xff1a;Betty’s blog 为了让我们更加深入理解vector&#xff0c;接下来我们将模拟实现一个简易版的vect…

二叉树链式结构的实现(递归的暴力美学!!)

前言 Hello,小伙伴们。你们的作者菌又回来了&#xff0c;前些时间我们刚学习完二叉树的顺序结构&#xff0c;今天我们就趁热打铁&#xff0c;继续我们二叉树链式结构的学习。我们上期有提到&#xff0c;二叉树的的底层结构可以选为数组和链表&#xff0c;顺序结构我们选用的数…

将YOLOv8模型从PyTorch的.pt格式转换为OpenVINO支持的IR格式

OpenVINO是Open Visual Inference & Neural Network Optimization工具包的缩写&#xff0c;是一个用于优化和部署AI推理模型的综合工具包。OpenVINO支持CPU、GPU和NPU设备。 OpenVINO的优势: (1).性能&#xff1a;OpenVINO利用英特尔CPU、集成和独立GPU以及FPGA的强大功能提…

PHP学习:PHP基础

以.php作为后缀结尾的文件&#xff0c;由服务器解析和运行的语言。 一、语法 PHP 脚本可以放在文档中的任何位置。 PHP 脚本以 <?php 开始&#xff0c;以 ?> 结束。 <!DOCTYPE html> <html> <body><h1>My first PHP page</h1><?php …

3千米以上音视频键鼠延长解决方案:KVM光纤延长器

KVM光纤延长器​​​​​​​是什么&#xff1f; KVM光纤延长器是一种使用光纤来传输键盘、视频和鼠标&#xff08;KVM&#xff09;信号的设备&#xff0c;由发送端和接收端组成&#xff0c;一般成对使用。它可以让用户在远离电脑的地方如同在本地一样方便快捷的操作电脑。 KV…

mysql数据库基础语法(未完)

数据库的超级用户是root 一、注释 &#xff08;1&#xff09;“-- ”减号减号空格 注意不要省略空格 &#xff08;2&#xff09;“#” 井号 二、数据库操作 1、创建 CREATE DATABASE [IF NOT EXISTS] <数据库名> [CHARACTER SET utf8] 2、删除 DROP DATABASE …

MySQL —— 初始数据库

数据库概念 在学习数据库之前&#xff0c;大家保存数据要么是在程序运行期间&#xff0c;例如&#xff1a;在学习编程语言的时候&#xff0c;大家写过的管理系统&#xff0c;运用一些简单的数据结构&#xff08;例如顺序表&#xff09;来组织数据&#xff0c;可是程序一旦结束…

硬盘数据丢失不再怕,四大恢复工具帮你轻松逆转局面!

硬盘故障、误删文件、病毒攻击等原因导致数据丢失的情况时有发生。面对这种情况&#xff0c;如何高效、快速地进行硬盘数据恢复呢&#xff1f;接下来几款好用的数据恢复软件推荐给大家。 一、福昕数据恢复&#xff1a;全方位恢复&#xff0c;让数据无遗漏 链接&#xff1a;ww…

Windows(Win10、Win11)本地部署开源大模型保姆级教程

目录 前言1.安装ollama2.安装大模型3.安装HyperV4.安装Docker5.安装聊天界面6.总结 点我去AIGIS公众号查看本文 本期教程用到的所有安装包已上传到百度网盘 链接&#xff1a;https://pan.baidu.com/s/1j281UcOF6gnOaumQP5XprA 提取码&#xff1a;wzw7 前言 最近开源大模型可谓闹…

观测器控制仿真案例详解(s-function函数)

目录 一、弹簧-质量-阻尼系统1. 系统状态空间方程2. 观测器状态空间方程 二、仿真(Simulink s-function函数)1. 搭建Simulink仿真模型2. s-function函数代码3. 仿真效果 控制理论–观测器设计 一、弹簧-质量-阻尼系统 系统参数&#xff1a; m 1 , K 1 , B 0.5 m 1\,, K 1…

【前端面试题】后端一次性返回10w条数据,该如何渲染?

后端一次返回 10w 条数据&#xff0c;本身这种技术方案设计就不合理。 问题分析&#xff1a; JS 支持处理10w 条数据&#xff0c;但 DOM 一次渲染 10w 条数据&#xff0c;可能会卡顿&#xff0c;所以需想办法减少 DOM 渲染 若非要实现&#xff0c;则可以考虑以下两种方案 自…

【C语言】程序环境,预处理,编译,汇编,链接详细介绍,其中预处理阶段重点讲解

目录 程序环境 翻译环境 1. 翻译环境的两个过程 2. 编译过程的三个阶段 执行环境 预处理(预编译) 1. 预定义符号 2. #define 2.1 用 #define 定义标识符(符号) 2.2 用 #define 定义宏 2.3 #define 的替换规则 2.4 # 和 ## 的用法 2.5 宏和函数 2.6 #undef …

Java小白入门到实战应用教程-权限修饰符

Java小白入门到实战应用教程-权限修饰符 前言 在前面的内容中我们其实已经接触到了权限修饰符&#xff1a;public 在java中权限修饰符除了public外&#xff0c;还有private、protected、默认权限。 权限修饰符可用来修饰类、成员变量、方法(函数)。 其中修饰类只能用publi…