浅析C++11中的右值引用、转移语义和完美转发
1. 左值与右值: C++对于左值和右值没有标准定义,但是有一个被广泛认同的说法:可以取地址的,有名字的,非临时的就是左值;不能取地址的,没有名字的,临时的就是右值. 可见立即数,函数返回的值等都是右值;而非匿名对象(包括变量),函数返回的引用, 从本质上理解,创建和销毁由编译器幕后控制的,程序员只能确保在本行代码有效的,就是右值(包括立即数);而用户创建的,通过作用域规则可知其生存期的,就是左值(包括函数返回的局部变量的引用以及 int& foo(){int tmp; return tmp;} int fooo(){int tmp; return tmp;} int a=10; const int b; int& temp=foo();//虽然合法,但temp引用了一个已经不存在的对象 int tempp=fooo(); 以上代码中,a,temp和 一般来说,编译器是不允许对右值进行更改的(因为右值的生存期不由程序员掌握,即使更改了右值也未必可以用),对于内置类型对象尤其如此,但C++允许使用右值对象调用成员函数,虽然允许这样做,但出于同样原因,最好不要这么做. 2. 右值引用: 右值引用的表示方法为 Datatype&& variable 右值引用是C++ 11新增的特性,所以C++ 98的引用为左值引用.右值引用用来绑定到右值,绑定到右值以后本来会被销毁的右值的生存期会延长至与绑定到它的右值引用的生存期,右值引用的存在并不是为了取代左值引用,而是充分利用右值(特别是临时对象)的建构来减少对象建构和析构操作以达到提高效率的目的,例如对于以下函数: (Demo是一个类) Demo foo(){ Demo tmp; return tmp; } 在编译器不进行RVO(return value optimization)优化的前提下以下操作: Demo x=foo(); 将会调用三次构造函数(tmp的,x的,临时对象的),相应的在对象被销毁时也会调用三次析构函数,而如果采用右值引用的方式: Demo&& x=foo(); 那么就不需要进行x的建构,本来本来要被销毁的临时对象也会由于x的绑定而将生存期延长至和x一样(可以理解为x赋予了那个临时对象一个合法地位:一个名字),就需要提高了效率(代价就是tmp需要占据4字节空间,但这是微不足道的). 右值引用与左值引用绑定规则: 常量左值引用可以绑定到常量和非常量左值,常量和非常量右值; 非常量左值引用只能绑定到非常量左值; 非常量右值引用只能绑定到非常量右值(vs2013也可以绑定到常量右值); 常量右值引用只能绑定到常量和非常量右值(非常量右值引用只是为了语义的完整而存在,常量左值引用就可以实现它的作用). 虽然从绑定规则中可以看出常量左值引用也可以绑定到右值,但显然不可以改变右值的值,右值引用就可以,从而实现转移语义,因为右值引用通常要改变所绑定的右值,所以被绑定的右值不能为 注意:右值引用是左值! 3. 转移语义(move semantics): 右值引用被引入的目的之一就是实现转移语义,转移语义可以将资源 ( 堆,系统对象等 ) 的所有权从一个对象(通常是匿名的临时对象)转移到另一个对象,从而减少对象构建及销毁操作,提高程序效率(这在2的例子中已经作了解释).转移语义与拷贝语义是相对的.从转移语义可以看出,实际上,转移语义并不是新的概念,它实际上已经在C++98/03的语言和库中被使用了,比如在某些情况下拷贝构造函数的省略(copy constructor elision in some contexts),智能指针的拷贝(auto_ptr “copy”),链表拼接(list::splice)和容器内的置换(swap on containers)等,只是还没有统一的语法和语义支持 虽然普通的函数和操作符也可以利用右值引用实现转移语义(如2中的例子),但转移语义通常是通过转移构造函数和转移赋值操作符实现的.转移构造函数的原型为 例如: class Demo{ public: Demo():p(new int[10000]{}; Demo(Demo&& lre):arr(lre.arr),size(lra.size){lre.arr=NULL;}//转移构造函数 Demo(const Demo& lre):arr(new int[10000]),size(arr.size){ for(int cou=0;cou<10000;++cou) arr[cou]=lew.arr[cou]; } private: int size; int* arr; } 从以上代码可以看出,拷贝构造函数在堆中重新开辟了一个大小为10000的 Demo(Demo&& lre):arr(lre.arr),size(lre.size)({lre.arr=NULL;}
因为 4. move()函数 3中的例子并非万能,Demo(Demo&& lre)的实参必须是右值,有时候一个左值即将到达生存期,但是仍然想要使用转移语义接管它的资源,这时就需要 5. 完美转发(perfect forwarding) 完美转发指的是将一组实参"完美"地传递给形参,完美指的是参数的 void func(const int); void func(int); void func(int&&); 如果要将它们包装到一个函数 void cover(typename para){ func(para); } 使得针对不同实参能在 template<typename T> void cover(T para){ ... func(para); ... } 但如果传递的是一个相当大的对象,又会造成效率问题,要通过引用传递实现形参与实参的完美匹配(包裹 函数形参 T的类型 推导后的函数形参 T& A& A& 因此,对于前例的函数包装要求,采用以下模板就可以解决: template<typename T> void cover(T&& para){ ... func(static_cast<T &&>(para)); ... }
如果传入的是左值引用,转发函数将被实例化为: void func(T& && para){ func(static_cast<T& &&>(para)); } 应用引用折叠,就为: void func(T& para){ func(static_cast<T&>(para)); } 如果传入的是右值引用,转发函数将被实例化为: void func(T&& &¶){ func(static_cast<T&& &&>(para)); } 应用引用折叠,就是: void func(T&& para){ func(static_cast<T&&>(para)); } 对于以上的 所以最终版本为 template<typename T> void cover(T&& para){ func(forward(forward<T>(para))); }
总结 以上就是关于C++11中右值引用、转移语义和完美转发的全部内容,这篇文章介绍的很详细,希望对大家的学习工作能有所帮助。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |