C++11中引用了右值引用和移动语义,可以避免无谓的复制,提高了程序性能。
一、左值和右值
左值是表达式结束后仍然存在的持久对象,右值是指表达式结束时就不存在的临时对象。
区分表达式的左右值属性:如果可对表达式用&符取址,则为左值,否则为右值
比如:
int a=10; int b=a;
对于a=10,由于10是临时的,表达式结束这个值就不存在了,因此是右值。而a=10中,a作为接受的变量,是表达式结束后仍然存在的对象,因此是左值。
对于b=a,由于a、b都是持久存在的,因此都是左值。
二、左值引用&
&
只能用作左值引用,因此对于下面的test(int& x)
,中的参数x一定是左值
对于一个常数10
,它是右值,因此没法调用test(10);
void test(int& x){//这个x一定是左值 cout<<x<<endl; } int main(){ int a=10; //test(10); //传入的是右值,因此会报错 test(a); //传入的是左值 }
三、右值引用&&
右值引用就是对一个右值进行引用的类型。因为右值没有名字,所以我们只能通过引用的方式找到它。
无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所把绑定对象的内
存,只是该对象的一个别名。
通过右值引用的声明,该右值又“重获新生”,其生命周期其生命周期与右值引用类型变量的生命周期一
样,只要该变量还活着,该右值临时量将会一直存活下去。
如下面代码,10作为右值,正常情况下,在使用完后该临时变量就应该消失了。但是int&& x
相当于给这个10取了别名为x,也叫右值引用。它的好处是避免了拷贝。
void test(int&& x){ cout<<x<<endl; } int main(){ int a=10; test(10); }
&&总结:
- 左值和右值是独立于它们的类型的,右值引用类型可能是左值也可能是右值(也就是说对于&,只能用作左值引用,而&&既可以作为左值引用也可以作为右值引用,取决于传入的参数是左值还是右值)
- auto&& 或函数参数类型自动推导的 T&& 是一个未定的引用类型,被称为 universal references,
它可能是左值引用也可能是右值引用类型,取决于初始化的值类型。 - 所有的右值引用叠加到右值引用上仍然是一个右值引用,其他引用折叠都为左值引 用。当 T&& 为
模板参数时,输入左值,它会变成左值引用,而输入右值时则变为具名的右 值引用 - 编译器会将已命名的右值引用视为左值,而将未命名的右值引用视为右值
应用:右值引用优化性能,避免深拷贝
A(const A& a)
是拷贝构造函数,如果没有移动构造函数A(A&& a)
的情况下,执行 A a = Get(false)
,调用的是拷贝构造函数,它会将Get(flase)返回的结果拷贝给a,然后执行析构函数,但是这样会造成性能的损耗。
因此可以使用移动构造函数A(A&& a)
,由于Get(false)
得到的是一个右值,因此会调用移动构造函数,只需要将需要构造的对象的指针拷贝就行了,将被移动的对象的指针置为空,就完成了移动拷贝构造。(避免了深拷贝)
移动语义可以将资源(堆、系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能够减少
不必要的临时对象的创建、拷贝以及销毁,可以大幅度提高 C++ 应用程序的性能,消除临时对象的维护
(创建和销毁)对性能的影响。
#include <iostream> using namespace std; class A { public: A() :m_ptr(new int(0)) { cout << "constructor A" << endl; } A(const A& a) :m_ptr(new int(*a.m_ptr)) { cout << "copy constructor A" << endl; } A(A&& a) :m_ptr(a.m_ptr) { a.m_ptr = nullptr; cout << "move constructor A" << endl; } ~A(){ cout << "destructor A, m_ptr:" << m_ptr << endl; if(m_ptr) delete m_ptr; } private: int* m_ptr; }; // 为了避免返回值优化,此函数故意这样写 A Get(bool flag) { A a; A b; cout << "ready return" << endl; if (flag) return a; else return b; } int main() { { A a = Get(false); // 正确运行 } cout << "main finish" << endl; return 0; }
四、移动语义 move
我们知道移动语义是通过右值引用来匹配临时值的,那么,普通的左值是否也能借组移动语义来优化性
能呢?C++11为了解决这个问题,提供了std::move()方法来将左值转换为右值,从而方便应用移动语
义。move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝
int main() { MyString a; a = MyString("Hello"); // move MyString b = a; // copy MyString c = std::move(a); // move, 将左值转为右值 return 0; }
五、完美转发forward
forward 完美转发实现了参数在传递过程中保持其值属性的功能,即若是左值,则传递之后仍然是左
值,若是右值,则传递之后仍然是右值。
对于一个函数
Template<class T> void func(T &&val);
根据前面所描述的,这种引用类型既可以对左值引用,亦可以对右值引用。
但要注意,引用以后,这个val值它本质上是一个左值!
看下面例子
int &&a = 10; int &&b = a; //错误
注意这里,a是一个右值引用,但其本身a也有内存名字,所以a本身是一个左值,再用右值引用引用a这
是不对的
因此我们有了std::forward()完美转发,这种T &&val中的val是左值,但如果我们用std::forward (val),
就会按照参数原来的类型转发;
int &&a = 10; int &&b = std::forward<int>(a);
例子:
#include <iostream> using namespace std; template <class T> void Print(T &t) { cout << "L" << t << endl; } template <class T> void Print(T &&t) { cout << "R" << t << endl; } template <class T> void func(T &&t) // 左值右值都只能走这个 { cout << "func(T &&t)" << endl; Print(t); // L (什么都不做,默认当作左值) Print(std::move(t)); // 肯定调用"R" Print(std::forward<T>(t)); // 不确定L R ,如果要完美往下层函数转发加forward } int main() { cout << "-- func(1)" << endl; func(1); // 1是R int x = 10; int y = 20; cout << "-- func(x)" << endl; func(x); // x本身是左值 (因此后续调用Print(std::forward<T>(t))的时候,传入的依然是左值) cout << "-- func(std::forward<int>(y))" << endl; func(std::forward<int>(y)); // std::forward<int>(y)变成右值 (如果传入的参数,本身不带有&&符号,那么forward就把它当作右值) return 0; }
可以看到结果,分别是3个fun调用的输出。
对于func(1)
,1
是右值。func(T &&t)
因此这边t的语言应该是右值,但是由于编译器会将已命名的右值看作左值,因此Print(t)
中传入的是左值,Print(std::move(t))
这里肯定是调用右值,因为显示地转为右值了。Print(std::forward(t))
,传入的值是根据&&类型的值,原来的类型决定,因为传入&&前,是右值,因此Print(std::forward(t))
在forward调用后是右值。
其他3种func情况也是类似的理解。
另外对于 func(std::forward(y));
由于y本身并不带有&&符号,因此forward后,默认是变成右值的。(什么是带&&符号?就是func(T &&t)
这个函数的参数带了&&)