当前位置:首页 » 《关注互联网》 » 正文

【C++】右值引用全面揭秘:解锁 C++11 的性能革命与移动语义奥秘!

25 人参与  2024年12月03日 10:01  分类 : 《关注互联网》  评论

点击全文阅读


文章目录

右值和左值的基本概念左值引用和右值引用右值引用的主要用途移动语义只有拷贝构造和赋值重载而没有移动语义的传值返回增加移动构造和移动赋值的传值返回 引用折叠与完美转发
C++11 引入了右值引用,这是C++语言的一个重要特性,目的是为了提高程序的性能,尤其在对象的传递和资源管理方面。
右值引用和左值引用相比,解决了左值引用在传返回值的不足,显著减少了不必要的拷贝,提高效率。

右值和左值的基本概念

在 C++ 中,表达式的值可以分为左值和右值两种类型:

左值:表示一个持久存在的对象或者内存位置,通常在赋值语句的左侧出现,以及有可以取地址的特性。例如:变量、数组元素、解引用等都是左值。
//以下均是左值//变量int a = 3;int* pa = &a;const int b = a;int* ptr = new int(3);//解引用*ptr = 4;//数组元素string str("abcdef");str[0];
右值:表示临时对象、字面量常量或者表达式的结果,通常只能出现在赋值语句的右侧,有不可取地址的特性。右值是没有名称的、即将被销毁的对象。
int a = 4, b = 5;//以下均是右值100;a + b;fmin(x, y);string("qwer");

左值引用和右值引用

引用就是给对象取别名,右值引用就是给右值取别名左值引用就是给右值取别名右值引用左值引用在语法形式上是类似的:

Type& ref = x;  //左值引用Type&& rref = y;  //右值引用

可以看到,左值引用是用 & ,而右值引用是用 &&

//左值int a = 3;int* pa = &a;const int b = a;int* ptr = new int(3);//左值引用int& ra = a;int*& rpa = pa;const int& rb = b;int* rptr = ptr;
int a = 4, b = 5;//右值//100;//a + b;//fmin(x, y);//string("qwer");//右值引用int&& rr1 = 100;int&& rr2 = a + b;int&& rr3 = fmin(a, b);string&& rr4 = string("qwer");

对右值引用的理解:右值本质上是一种生命周期很短的对象(将亡值),而右值引用实际上是将该对象的地址保存,该对象就不会立即销毁,延长了生命周期。

注意,通过右值引用创建出来的对象的属性是左值,这一点非常的重要,涉及到下面提及的完美转发。


一般而言,右值引用只能引用右值,左值引用只能引用左值,但在特殊情况下,右值引用可以引用左值,左值应用也可以引用右值 。

左值引用去引用右值:需要在前面加 const 修饰。右值引用去引用左值:需要对左值进行 move
//左值引用去引用右值,需要加constconst int& r1 = 10;const string& r2 = string("abcd");//右值引用求引用左值,需要对左值moveint x = 3;int&& rr1 = move(x);string str("1234");string&& rr2 = move(str);

左值引用在特定条件下可以引用右值,这一点在前面其实也有所涉及,之前模拟实现容器(如 vector、list等)的 push_back 函数: void push_back (const T& x),加 const 是为了让 x 既能接收左值也能接收右值。

move 本质上就是强制类型转换,不会改变左值对象本身的属性。

template <class T>typename remove_reference<T>::type&& move (T&& arg) noexcept;{return static_cast<remove_reference<decltype(arg)>::type&&>(arg)}

右值引用的主要用途

在右值引用出现之前,左值引用还是无法解决在某些场景下需要传值返回的问题,而右值引用的出现,实现了移动语义完美转发,显著提高C++程序在对象的的拷贝和传递的性能。

移动语义

移动语义可以分为移动构造移动赋值,其实就可以“移动”资源而不是复制资源。从而避免不必要的资源拷贝。右值引用允许了资源从一个对象转移到另一个对象,而不是创建一个新的副本。

接下来,我们就以一个自定义 string 类,来看看移动语义的作用是多么强大,还有在没有移动语义之前VS的设计者如何跟冗余构造斗智斗勇。

class string{public://构造string(const char* str = "") {cout << "string(char* str) -- 构造" << endl;_size = strlen(str);_capacity = _size;_str = new char[_capacity + 1];  strcpy(_str, str);               }//析构~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}//拷贝构造string(const string& s){cout << "string(const string& s) -- 拷贝构造" << endl;string tmp(s._str);swap(tmp);}//赋值重载string& operator=(const string& s){cout << "string& operator=(string s) -- 赋值重载" << endl;if (this != &s){_str[0] = '\0';if (s._capacity > _capacity){char* tmp = new char[s._capacity + 1];if (_str)delete[] _str;_str = tmp;_capacity = s._capacity;}strcpy(_str, s._str);_str[s._capacity] = '\0';_size = s._size;}return *this;}private:char* _str;size_t _size;size_t _capacity;};

只有拷贝构造和赋值重载而没有移动语义的传值返回

正常对一个自定义类传值返回是需要进行3次构造的,函数体内将构造需要返回 str 对象,在返回 str 时先对其拷贝构造出一个临时对象 tmp ,函数体外的用于接收返回值的 ret 再去拷贝构造这个 tmp 对象,很明显,这样多次构造消耗很大,效率很低。如下图:
在这里插入图片描述
聪明的编译器设计师一想,这样不慢了啊,干脆不构建临时对象,直接将 str 拷贝构造给 ret 不就行了。如下图:
在这里插入图片描述
另一位设计师看了,不行啊,你这样还是慢,看我的,直接将3次构造合三为一。如下图:
在这里插入图片描述
我们可以运行一下来验证结果,如下图:
在这里插入图片描述
结果就是编译器真的做出了 “合3为1”的极致优化来提高效率,这里真的不得不感叹下设计编译器的设计师能力是真的强?。

如果用于接收的 ret 是已经存在的变量,那么走的就是就是赋值重载,跟上面的情况类似。
在这里插入图片描述
在这里插入图片描述
这里编译器就不能做合3为1 的优化了,因为在赋值重载之前可能会对 ret 进行其他的操作。

增加移动构造和移动赋值的传值返回

移动构造和移动赋值本质上就是掠夺资源,即使在函数体内的 str 对象是左值,但是它是临时对象,出了作用域就销毁了,所以可以将其视作特殊的右值,这里隐式地调用 move j将 str 转成右值,从而调用移动构造或者移动赋值。

void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}//移动构造string(string&& s){cout << "string(string&& s) -- 移动构造" << endl;swap(s);}//移动赋值string& operator=(string&& s){cout << "string& operator=(string&& s) -- 移动赋值" << endl;swap(s);return *this;}

可以看出,移动构造移动赋值的效率对比拷贝构造赋值重载来说是非常高的,因为是利用 swap 函数交换右值的资源。


有了移动语义后,上面的传值返回的情况就会变成如下的情况:
在这里插入图片描述
VS2022 上,上面的情况哪怕是有了移动构造,还是会被极致优化成一个构造,测不出走移动构造的结果。
在这里插入图片描述
而对于移动赋值,编译器优化没那么厉害,可以测得出移动赋值的结果。
在这里插入图片描述
在这里插入图片描述


注意:知道了移动语义后,这里有必要提一个点,那就是不要轻易使用 move 函数将左值强制转换成右值,因为这样可能会触发移动构造导致原本左值的自资源被转移走,这是十分危险的。

举个例子:
在这里插入图片描述

在这里插入图片描述
可以看到,将 str1 进行 move 之后再赋值给 str2 , 触发了 移动构造str2 的资源被转移到了 str1 ,这显然是非常不合理的,所以对于 move 要慎重使用。

引用折叠与完美转发

引用折叠是C++11引入的重要概念,与右值引用和模板的结合密切相关。引用折叠的目的是为了帮助解决模板中出现的多重引用类型,使得代码更加简洁和一致。

当我们再模板中使用右值引用 && 时,可能会遇到多重引用类型的问题:

template<class T>void func(T&& x){}int main(){   int a = 0;   int& b = a;   int&& c = 10;      //多重引用的问题   func(a);       //-> int& &   func(b);       //-> int& &&   func(c);   //-> int&& &&}

在上面的例子中,由于传入的值的类型各不一样,导致多重引用类型的问题。

有人可能会疑惑,为什么右值引用的函数能传入左值a?

因为这里的 && 其实不代表右值引用,当你传左值时,函数会将其识别成左值的引用 T& ,然后触发引用折叠,成为一个左值引用。换句话说 && 并不是右值引用,而是万能引用,这种函数既能接收左值也能接收右值

引用折叠的规则如下:

T& & → T&:左值引用与左值引用折叠为一个左值引用。T& && → T&:左值引用与右值引用折叠为一个左值引用。T&& & → T&&:右值引用与左值引用折叠为一个右值引用。T&& && → T&&:右值引用与右值引用折叠为一个右值引用。

为了进一步理解引用折叠,我们可以通过一个简单的例子来观察它是如何工作的。

#include <iostream>template <typename T>void f(T&& arg) {    cout << "T&&" << endl;}template <typename T>void g(T& arg) {    cout << "T&" << endl;}int main() {    int x = 10;    f(x);         // T& & -> T&,输出 "T&"    f(20);        // T&& && -> T&&,输出 "T&&"    g(x);         // T& & -> T&,输出 "T&"}

总结一下,对于函数 func(T&& x) 来说,只有传入的值是右值引用类型的 x ,才能是右值引用其余情况均被引用折叠成左值引用


对于这种 Func (T&& x) 函数模板,无论传入什么类型的值,x 的值都是左值,因为左值引用的值是左值,右值引用创建出来的值也是左值,这一点我们在上面提到过。

如果我们在函数内再次调用其他函数,可能会因为参数属性退化成左值导致不能正确调用其他函数。

void func(int& x) {cout << "左值引用" << endl;}void func(const int& x) {cout << "const 左值引用" << endl;}void func(int&& x) {cout << "右值引用" << endl;}void func(const int&& x) {cout << "const 右值引用" << endl;}template <class T>void forwarder(T&& arg) {func(arg);  }int main() {//参数属性会退化成左值,一下输出均是左值引用输出int a = 10;forwarder(a);        forwarder(20);       const int b = 10;forwarder(b);        forwarder(move(b));}

在这里插入图片描述

想要解决这种问题,就需要用到完美转发 std::forward ,他会自动处理参数的类型,确保传递给下一层的函数的参数保持器原有的属性(左值或者右值)。

template <typename T>T&& forward(typename std::remove_reference<T>::type& arg) noexcept {    return static_cast<T&&>(arg);}

有了完美转发 std::forward 后,上面的例子就能正确调用了。

void func(const int& x) {cout << "const 左值引用" << endl;}void func(int&& x) {cout << "右值引用" << endl;}void func(const int&& x) {cout << "const 右值引用" << endl;}template <class T>void forwarder(T&& arg) {func(forward<T>(arg));  // 完美转发,确保转发时引用类型正确}int main() {int a = 10;forwarder(a);        // 传入左值,输出 "左值引用"forwarder(20);       // 传入右值,输出 "右值引用"const int b = 10;forwarder(b);        // 传入const 左值,输出 "const 左值引用"forwarder(move(b));  // 传入const 右值,输出 "const 右值引用"}

在这里插入图片描述

在这个例子中,forwarder(T&& arg) 是一个函数模板,使用了 T&& ,可以接受任何类型的参数(无论左值和右值)。std::forward<T>(arg) 会根据传入的参数类型,自动选择是转发为左值引用还是右值引用。引用折叠在这里的作用是,确保当我们在模板中使用右值引用时,最终传递给func 的参数是合适的引用类型。


拜拜,下期再见?

摸鱼ing?✨?
请添加图片描述


点击全文阅读


本文链接:http://zhangshiyu.com/post/195805.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

最新文章

  • 星辰藏眼底,万般皆过客完整版(傅清予江宴离)全文免费阅读无弹窗大结局_(傅清予江宴离)星辰藏眼底,万般皆过客完整版免费阅读全文最新章节列表_笔趣阁(星辰藏眼底,万般皆过客完整版) -
  • 青色似玉月如钩全文阅读(谢语乔沈寒声)全文免费阅读无弹窗大结局_(谢语乔沈寒声)青色似玉月如钩全文阅读小说最新章节列表_笔趣阁(青色似玉月如钩全文阅读) -
  • 闪婚后,冷面霸总是个妻管严顾鸿宇沈绾绾完结小说免费阅读_完结小说闪婚后,冷面霸总是个妻管严顾鸿宇沈绾绾 -
  • 小千金她又在刻意装乖,高冷疯批他心动了精彩小说墨施霍黛(小千金她又在刻意装乖,高冷疯批他心动了精彩小说)全文免费阅读无弹窗大结局_(墨施霍黛免费阅读全文大结局)最新章节列表_笔趣阁(墨施霍黛) -
  • 小千金她又在刻意装乖,高冷疯批他心动了在线阅读(墨施霍黛)抖音热文_《小千金她又在刻意装乖,高冷疯批他心动了在线阅读》最新章节免费在线阅读 -
  • 《假少爷重生后成了万人迷》沈随江乔完本小说免费阅读_完本完结小说《假少爷重生后成了万人迷》沈随江乔 -
  • 重生后,夫人自爆马甲要护夫完整版简星尘司墨寒(简星尘司墨寒)全文免费阅读无弹窗大结局_(简星尘司墨寒)重生后,夫人自爆马甲要护夫完整版小说最新章节列表_笔趣阁(简星尘司墨寒) -
  • 刘芯李洲的小说叫什么_夹免费全文热门完本小说_夹免费全文(刘芯李洲)完本小说
  • 一口气看完小说《安溪姜悠然》安溪姜悠然完整版《安溪姜悠然》大结局爆款小说
  • 女儿被打到引产,我带着首长杀疯了结局免费小说在线阅读_小说完结推荐女儿被打到引产,我带着首长杀疯了结局张峰高强热门小说
  • 热门网络小说我去国外赎男友,他扭头把我送绑匪:番外+全文+后续_我去国外赎男友,他扭头把我送绑匪:番外+全文+后续完结版小说阅读_热门网络小说我去国外赎男友,他扭头把我送绑匪:番外+全文+后续
  • 小说免费阅读无弹窗我去国外赎男友,他扭头把我送绑匪:番外+全文+后续(江禾邵伟)_我去国外赎男友,他扭头把我送绑匪:番外+全文+后续(江禾邵伟)完结免费小说

    关于我们 | 我要投稿 | 免责申明

    Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1