C++11出现的右值相关语法可谓是不少C++程序员难以理解的新特性,很多人知其然而不知其因此然,面试被问到时大概就只知道能够减小开销,可是为何减小开销、减小了多少开销、何时用...这些问题也不必定知道,因而我写下了这篇夹带本身理解的博文,但愿它对你有所帮助。程序员
在介绍右值引用等概念以前,能够先来认识下浅拷贝(shallow copy)和深拷贝(deep copy)。面试
这里举个例子:函数
class Vector{ int num; int* a; public: void ShallowCopy(Vector& v); void DeepCopy(Vector& v); };
//浅拷贝 void Vector::ShallowCopy(Vector& v){ this.num = v.num; this.a = v.a; }
//深拷贝 void Vector::DeepCopy(Vector& v){ this.num = v.num; this.a = new int[num]; for(int i=0;i<num;++i){a[i]=v.a[i]} }
能够看到,深拷贝的开销每每比浅拷贝大(除非没有指向动态分配内存的属性),因此咱们就倾向尽量使用浅拷贝。优化
可是浅拷贝的有一个问题:当有指向动态分配内存的属性时,会形成多个对象共用这块动态分配内存,从而可能致使冲突。一个可行的办法是:每次作浅拷贝后,必须保证原始对象再也不访问这块内存(即转移全部权),这样就保证这块内存永远只被一个对象使用。this
那有什么对象在被拷贝后能够保证再也不访问这块内存呢?相信你们内心都有答案:临时对象。指针
为了让编译器识别出临时对象,从而好作浅拷贝优化,因而C++引入了左值(lvalue)、右值(rvalue)的概念。code
之因此取名左值右值,是由于在等式左边的值每每是持久存在的左值类型,在等式右边的表达式值每每是临时对象。对象
a = ++b; a = b+c*2; a = func();
更直观的理解是:有变量名、能够取地址的对象都是左值,没有变量名、不能够取地址的都是右值。(由于有无变量名意味着这个对象是否在下一行代码时依然存在)内存
有了左值、右值的概念,咱们就很清楚认识到右值都是些短暂存在的临时对象。编译器
因而,C++11 为了匹配这些左右值类型,引入了右值引用类型 && 。
右值引用类型负责匹配右值,左值引用则负责匹配左值。
所以刚刚的浅拷贝、深拷贝例子,咱们能够无需显式调用浅拷贝或深拷贝函数,而是调用重载函数:
//左值引用形参=>匹配左值 void Vector::Copy(Vector& v){ this.num = v.num; this.a = new int[num]; for(int i=0;i<num;++i){a[i]=v.a[i]} } //右值引用形参=>匹配右值 void Vector::Copy(Vector&& temp){ this.num = temp.num; this.a = temp.a; }
固然,最标准仍是编写成各类构造函数(拷贝构造、移动构造、赋值构造、移动赋值构造):
移动的意思是转移全部权。因为右值 大部分 都是临时的值,临时值释放后也就再也不持有属性的全部权,所以这至关于转移全部权的行为。
//拷贝构造函数:这意味着深拷贝 Vector::Vector(Vector& v){ this.num = v.num; this.a = new int[num]; for(int i=0;i<num;++i){a[i]=v.a[i]} } //移动构造函数:这意味着浅拷贝 Vector::Vector(Vector&& temp){ this.num = temp.num; this.a = temp.a; }
虽然从优雅的实现深、浅拷贝这个目的开始出发,C++11的移动语义能够不止用于浅拷贝,得益于它,咱们还能够作利用右值转移全部权的特性,在右值所占有的空间临时存放一些东西。
除了上面说的临时值,有些左值其实也很适合转移全部权:
void func(){ Vector result; //...DoSomehing with ans if(xxx){ans = result;} //如今我但愿把结果提取到外部的变量a上。 return; }
能够看到result赋值给ans后就再也不被使用,咱们指望它调用的是移动赋值构造函数。
可是result是一个有变量名的左值类型,所以ans = result 调用的是赋值构造函数而非移动赋值构造函数。
为了将某些左值当成右值使用,C++11 提供了 std::move 函数以用于将某些左值转成右值,以匹配右值引用类型。
这也是移动语义的由来:不管是临时值仍是被强转的左值,只要遵照转移全部权的保证,均可以使用移动语义。
void func(){ Vector result; //...DoSomehing with ans if(xxx){ans = std::move(result);} //调用的是移动赋值构造函数 return; }
有了上面的知识后,咱们来从新审视一下右值引用类型。
先看看以下代码:
void test(Vector& o) {std::cout << "为左值。" << std::endl;} void test(Vector&& temp) {std::cout << "为右值。" << std::endl;} int main(){ Vector a; Vector&& b = Vector(); //请分别回答:a、std::move(a)、b 分别是左值仍是右值? test(a); test(std::move(a)); test(b); }
答:a是左值,std::move(a)是右值,但b倒是左值。
在这里b虽然是 Vector&& 类型,但却由于有变量名(便可持久存在),被编译器认为是左值。
//即便函数返还值是临时值,但返还类型是左值引用类型,所以被认为是持久存在的左值。 Vector& func1(); //函数返还值为右值引用类型=>是短暂存在的右值。 Vector&& func2(); //函数返还值为正常类型=>是短暂存在的右值。 Vector func3();
结论:右值引用类型只是用于匹配右值,而并不是表示一个右值。所以,尽可能不要声明右值引用类型的变量,而只在函数形参使用它以匹配右值。
实际上C++ std::move的实现原理就是的强转右值引用类型并返还之,因为函数返还值类型是临时值,且返还的仍是右值引用类型(非左值引用类型),所以该返还值会被判断为右值。
void func1(Vector v) {return;} void func2(Vector && v) {return;} int main() { Vector a; Vector &b = a; Vector c; Vector d; //请回答:不开优化的版本下,调用如下函数分别有多少Copy Consturct、Move Construct的开销? func1(a); func1(b); func1(std::move(c)); func2(std::move(d)); }
实际上在不开优化的版本下,若是实参为右值,调用func1的开销只比func2多了一次移动构造函数和析构函数。
实参传递给形参,即形参会根据实参来构造。其结果是调用了移动构造函数;函数结束时则释放形参。
假若说对象的移动构造函数开销较低(例如内部仅一个指针属性),那么使用无引用类型的形参函数是更优雅的选择,并且还能接受左值引用类型或无引用的实参(尽管这两种实参都会致使一次Copy Consturct)。
那咱们在写通常函数形参的时候,有必要每一个函数都提供关于&&形参的重载版本吗?
回答:通常来讲是不必的。对象的移动构造(赋值)函数开销不大时,咱们能够只提供非引用类型和左值引用类型(避免Copy Construct)的重载版本,而没必要编写右值引用类型的重载版本。
Vector func1() { Vector a; return a; } Vector func2() { Vector a; return std::move(a); } Vector&& func3() { Vector a; return std::move(a); } int main() { //请回答:不开优化的版本下,执行如下3行代码分别有多少Copy Consturct、Move Construct的开销? Vector test1 = func1(); Vector test2 = func2(); Vector test3 = func3(); }
一样的道理,执行这3行代码实际上都没有任何Copy Construct的开销(这其中也有NRV技术的功劳),都是只有一次Move Construct的开销。
此外一提,func3是危险的。由于局部变量释放后,函数返还值仍持有它的右值引用。
所以,这里也不建议函数返还右值引用类型,同前面传递参数相似的,移动构造开销不大的时候,直接返还非引用类型就足够了(在某些特殊场合有特别做用,例如std::move的实现)。
结论:咱们应该把编写右值引用类型相关的任务放在对象的构造、赋值函数上,而非通常函数。从源头上出发,你就会发如今编写其它代码时就会天然而然享受到了移动构造、移动赋值的优化效果。
接下来的内容都是属于模板的部分了:万能引用、引用折叠、完美转发。这部分更加难以理解,不编写模板代码的话能够绕道了。
万能引用(Universal Reference):
也就是说编写模板函数时,只提供万能引用形参一个版本就能够匹配左值、右值,没必要编写多个重载版本。
template<class T> void func(T&& t){ return; } int main() { Vector a,b; func(a); //OK func(std::move(b)); //OK }
此外须要注意的是,使用万能引用参数的函数是最贪婪的函数,容易让须要隐式转换的实参匹配到不但愿的转发引用函数。例以下面代码:
template<class T> void f(T&& value); void f(int a); //当调用f(long类型的参数)或者f(short类型的参数),则不会匹配int版本而是匹配到万能引用的版本
使用万能引用遇到的第一个问题是推导类型会出现不正确的引用类型:例如当模板参数T为Vector&或Vector&&,模板函数形参为T&&时,展开后变成Vector& &&或者Vector&& &&。
template<class T> void func(T&& t){ return; } int main(){ func(Vector()); //模板参数T被推导为Vector&& }
但显然C++中是不容许对引用再进行引用的,因而为了让模板参数正确传递引用性质,C++定义了一套用于推导类型的引用折叠(Reference Collapse)规则:
全部的折叠引用最终都表明一个引用,要么是左值引用,要么是右值引用。
引用折叠 | & | && |
---|---|---|
& | & | & |
&& | & | && |
Example1:
func(Vector());
模板函数func的T被推导为Vector&&,形参object为T&&即展开后为Vector&& &&。因为折叠规则的存在,形参object最终被折叠推导为Vector&&类型。
Example2:
func(a);
模板函数func的T在这里被推导为Vector&,形参object为T&&即展开后为Vector& &&。因为折叠规则的存在,形参object最终被推导为Vector&类型。
当咱们使用了万能引用时,即便能够同时匹配左值、右值,但须要转发参数给其余函数时,会丢失引用性质(形参是个左值,从而没法判断到底匹配的是个左值仍是右值)。
//固然咱们也能够写成以下重载代码,可是这已经违背了使用万能引用的初衷(仅编写一个模板函数就能够匹配左值、右值) template<class T> void func(T& t){ doSomething(t); } template<class T> void func(T&& t){ doSomething(std::move(t)); }
完美转发(Perfect Forwarding):C++11提供了完美转发函数 std:forward<T> 。它能够在模板函数内给另外一个函数传递参数时,将参数类型保持本来状态传入(若是形参推导出是右值引用则做为右值传入,若是是左值引用则做为左值传入)。
因而如今咱们能够这样作了:
template<class T> void func(T&& object){ doSomething(std::forward<T>(object)); }
不借助std::forward<T>间接传入参数的话,不管object是左值引用类型,仍是右值引用类型,都会被视为左值。
std::forward<T>()的实现主要就一句return static_cast<T&&>(形参),实际上也是利用了折叠规则。从而接受右值引用类型时,将右值引用类型的值返还(返还值为右值)。接受左值引用类型时,将左值引用类型的值返还(返还值为左值)。
而std::move<T>()的实现还须要先移除形参的全部引用性质获得无引用性质的类型(假设为T2),而后再return static_cast<T2&&>(形参),从而保证不会发生引用折叠,而是直接做为右值引用类型的值返还(返还值为右值)。