文章目录
右值和左值的基本概念左值引用和右值引用右值引用的主要用途移动语义只有拷贝构造和赋值重载而没有移动语义的传值返回增加移动构造和移动赋值的传值返回 引用折叠与完美转发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?✨?