同步转载自:http://www.cnblogs.com/liyiwen/archive/2013/04/07/3006506.html
唐风
-----------------------------------------
右值引用,是 C++11 语言核心中最为重要的改进之一。右值引用给 C++ 带来了“Move语义”(“转移语义”),同时解决了模板编程中完美转发的问题(Perfect forwarding)。右值引用使 C++ 对象有能力甄别什么是(可以看作)临时对象,对于临时对象的拷贝可以做某种特别的处理,一般来说主要是直接传递资源的所有权而不是像一般地进行拷贝,这就是所谓的 move 语义了。完美转发则是指在模板编程的时候,各层级函数参数传递时不会丢失参数的“属性”(lvalue/rvalue, const?之类的)。这两个问题都对提高 C++ 程序的效率很有好处,让 C++ 的程序可以更加精确地控制资源和对象的行为,(我觉得,同时在一定程度上也提高的 C++ 程序形式上的“自由度”)。
只要英文基本上过得去,建议阅读下面几个网页的文章,他们都是世界级的专家,对 C++11 的理解比我准确得多:
1. 比如清楚地说明了"why",但是没有说明"how",在Google排好前啊,Scott Meyers也推荐了(下面)
C++ Rvalue References Explained
http://thbecker.net/articles/rvalue_references/section_01.html2. Effective C++ 的作者写的
Universal References in C++11—Scott Meyers
http://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers3. VC 的库函数开发组的老师写的,这个我觉得技术细节上说得很清楚。但是很长。把“how”说得很清楚。
Rvalue References: C++0x Features in VC10, Part 2
http://blogs.msdn.com/b/vcblog/archive/2009/02/03/rvalue-references-c-0x-features-in-vc10-part-2.aspx
如果想看简化的中文版,就继续往下,这篇基本上是上面资料3的笔记
一、如何区分左右值
要理解右值引用(还有我们一直在用的“引用”,以后叫左值引用),就要先搞清楚“左值、右值”是什么。首先,左值右值是针对“表达式”而言的,如果一个表达式所表示的内容,在这个表达式语句完了之后还能访问到(也就是有一个实实在在的名称代表它),那么这个表达式就是左值,否则就是右值。我的理解就是,“需要编译器为程序生成临时“匿名”变量来表示的表达式的值,就是右值”。但是人类又不是编译器,用这个方法来判断左右值并不方便。最简单的判断一个表达式是左值还是右值的方法是,“能不能对这个表达式取地址”。比如下面几个:
- int x = 0;
- x + 1; // rvalue
- x; // lvalue
- ++x; // lvalue
- x++; // rvalue
- int y[10];
- y[0]; // lvalue
- "literal string" // rvalue
之所以“能不能对这个表达式取地址”可以作为表达式是左值还右值的判断方法呢,因为 C++ 标准说取地址符只能应用在左值上。((C++03 5.3.1/2).)(有例外,参考资料3说 VC 对 C++ 可以开扩展选项,但是一般正直的程序员不会用它!:))。因为右值被看成是“匿名”变量(或者说,是编译器为我们加的变量),是程序中的幽灵变量,程序员不需要知道它的存在,更不应该去处理它,所以如果能对右值取地址,容易出现很多对程序造成危险的行为。
再举个比较常见的例子:
- x = a + b + c + d;
其实编译器会生成类似下面伪代码的“东西”
- temp1 = a + b;
- temp2 = temp1 + c;
- x = temp2 + d;
这个例子中的 temp?,就是右值(temp这个名字只有编译器知道,程序是不知道的)。
(原因我猜是两个,一是因为大部分CUP所支持的机器指令都是两操作符的(大多数的运算符也是两个操作数的),另一个是把复杂的式子用类似的方法化简成简单的式子(二叉树形式语法树)并最终翻译成汇编是编译器的成熟算法,词法分析基本上都是在干这个活。(纯猜测,非科班,没学过))
我们之所以会关注右值,主要的原因是,右值有时候会带来不必要的性能开销。还是举类似的例子,如果 x, y, z 是某个类(A)的对象,A 在构造的时候动态地申请资源(比如一块大的内存),在拷贝构造的函数中也一样,先申请一块同样大小的内存,然后把拷贝元对象的内容复制一份到刚申请到的内存中。那么,当我们写下 z = x + y 的时候,重载的操作符函数 + 产生的临时变量 temp,把 x + y 的值计算好放在temp中,(这个temp是不可避免的,因为+不应该改变操作数的值),然后 z 再调用拷贝构造函数,申请一大块内存,把 temp 的值拷贝过来,最后,temp再调用析构函数,把自己的内存释放掉。 其实如果斤斤计较下,就会发现其实 temp 只是一个临时的过渡,反正 z = x + y 这个语句结束之后 temp 就没有意义(也被析构了),那么何不把 temp 的内存换给 z 呢,这样 z 不需要申请请的内存,也不需要拷贝它的值,temp也不需要释放内存了,省下不少开销啊。
是的,我们当然想这么干,不过在 C++11 之前,程序没有办法分辨拷贝的对象是不是一个临时变量(可变的右值),是不是可以安全地从这个对象中把资源偷过来,我们只能写一种“拷贝构造函数”,是的,就是我们一直在写的那种“拷贝用”的构造函数。现在你明白了为什么 C++ 中要增加右值引用了,为的就是让程序可以分辨出一个对象是不是“幽灵对象”,然后再有区别地“对待”传入函数的对象。不过先别急,先看看引用的类型。
二、引用的类型
现在,C++11里的引用类型分为下面几种了:
- 可变左值引用 :type &
- 常左值引用:type const &
- 可变右值引用:type &&
- 常右值引用:type const &&
这几种引用在初始化的时候,能绑定到什么“值”上呢?遵从两个规则:
- 要遵照“常量正确”的原则,也就是非常量的引用(type & 和 type &&)不能绑定到常量上(常量左值,常量右值)
- 防止意外修改“幽灵变量”,所以可变左值引用(type &)不能绑定到右值上
总结一下就是下面这个绑定的关系图:
在C++03的时候,我们就已经知道type &和type const & 这两种类型的参数是可以参与函数重载的,现在 C++11 加入了两个新的引用类型,也是可以参与重载的,重载的规则如下:
- 不能违反初始化绑定规则
- 左值倾向于选择(常)左值引用,右值倾向于选择(常)右值引用(强)
- 非常量值倾向于选择非常量引用(弱)
还是有例子会比较容易理解:
- #include <iostream>
- #include <string>
- #include <iomanip>
- using namespace std;
- void reference_overload(string & str) {
- cout<<setw(15)<<str<<" => "<<"type & "<<endl;
- }
- void reference_overload(string && str) {
- cout<<setw(15)<<str<<" => "<<"type && "<<endl;
- }
- void reference_overload(string const & str) {
- cout<<setw(15)<<str<<" => "<<"type const & "<<endl;
- }
- void reference_overload(string const && str) {
- cout<<setw(15)<<str<<" => "<<"type cosnt && "<<endl;
- }
- string const GetConstString() {
- return string("const_rvalue");
- }
- int main() {
- string lvalue("lvalue");
- string const const_lvalue("const_lvalue");
- reference_overload(lvalue);
- reference_overload(const_lvalue);
- reference_overload(string("rvalues"));
- reference_overload(GetConstString());
- }
运行的结果如下:
符合我们预期的想像。但在实际中,一般不需要重载这四种,而只需要对:
- type const &
- type &&
两种引用类型进行重载就很有用了,只有这两种类型的重载时,会有什么样的匹配发生?
- #include <iostream>
- #include <string>
- #include <iomanip>
- using namespace std;
- void reference_overload(string && str) {
- cout<<setw(15)<<str<<" => "<<"type && "<<endl;
- }
- void reference_overload(string const & str) {
- cout<<setw(15)<<str<<" => "<<"type const & "<<endl;
- }
- string const GetConstString() {
- return string("const_rvalue");
- }
- int main() {
- string lvalue("lvalue");
- string const const_lvalue("const_lvalue");
- reference_overload(lvalue);
- reference_overload(const_lvalue);
- reference_overload(string("rvalues"));
- reference_overload(GetConstString());
- }
运行结果如下:
从上面的结果看到,C++11 的引用重载规则基础上,实现对 type const & 和 type && 的重载,程序就可以“区分”出,什么变量的值是可以“神不知鬼不觉地偷走的”,而什么变量的值是不可以动的。有了这个办法,类的设计者就可以设计出“move 语义”的拷贝构造函数了。
三、move !
其实后面比较轻松,没有多少要记的东西了,举个简单的例子
- A(const A& _right) {
- this->p = new int[100];
- memcpy(this->p, _right.p, 100);
- }
- A(A && _right) {
- this->p = _right.p;
- _right.p = nullptr;
- }
1到4行是拷贝构造函数,而5到8行是转移构造函数(move)。拷贝构造函数中,我们从一个(常)左值,或是常右值作为拷贝源来构造一个新对象,原对象是不可以做修改的,所以只能重新 new 一块新的区域,并把数据拷贝一份。但如果拷贝源对象是非常量右值,说明这是一个“无人关心的”变量,我们可以通过指针交换,获取变量所拥有的资源,而“源对象”的针对则指向空,反正这个构造一结束,这个无人关心的变量就析构了。
从上面的例子可以很容易看出,在合适的地方使用这种转义构造,可以很大的提高程序性能。
当然这些是不够的,还有很多地方,我们也想用转移构造,却可能出问题,比如说下面这个例子:
- A GetA() {
- A temp;
- // do sth.
- return temp;
- }
- A x = GetA()
作为程序员,我知道在 return temp 这个语句后,这个temp变量就没有用了,因此我很想能把 temp 这个变量的内容,用转移拷贝构造函数转移给变量x,但是,temp是个左值!转移拷贝构造函数不会被选中!没有办法了吗?如果我们可以把左值“转换”成右值,让编译器调用转移构造函数的话,那就好了。这是一非常“普遍的需求”,除了上面这个例子之外,我们还会遇到很多类似的情况,比如,我想用“赋值运算符”来实现拷贝构造函数的时候:
- A(const A& _right) {
- this->p = new int[100];
- memcpy(this->p, _right.p, 100);
- }
- A(A && _right) {
- this->p = nullptr
- *this = _right; // Unexpected thing happens
- }
- A& operator = (A && _right) {
- this->p = _right.p;
- _right.p = nullptr;
- return *this;
- }
- A& operator = (A const & _right) {
- this->p = new int[100];
- memcpy(this->p, _right.p, 100);
- return *this;
- }
第7行,我们期待它能调用第9行的“转移赋值运算符”,但实际上会调用第15行的“普通赋值运算符”,
因为在C++中
- 具名的左值是左值
- 不具名的左值引用是左值
- 具名的右值引用是左值
- 不具名的右值引用是右值
所以上面这个例子,最终并不能实现我们想要的效果。
怎么办呢?C++11提供了这样的办法:std::move,现在程序会是这样:
- A GetA() {
- A temp;
- // do sth.
- return std::move(temp);
- }
- A x = GetA()
好了,现在转移构造函数被调用了,像魔法对吧? std::move 这个名字取得不好,其实它本身没有“move”任何东西,它只是把“不是“非常量右值”转换成“非常量右值””。但这是怎么做到的呢?其实并不难:
- template <typename T> struct RemoveReference {
- typedef T type;
- };
- template <typename T> struct RemoveReference<T&> {
- typedef T type;
- };
- template <typename T> struct RemoveReference<T&&> {
- typedef T type;
- };
- template <typename T> typename RemoveReference<T>::type&& move(T&& t) {
- return t;
- }
有了这个 move 之后,就有办法把左值,右值,左值引用,右值引用都转换成右值引用了。
好了,到这里为止,基本上对于左值右值,以及它们的引用,还有转移语义,都应该清楚了。
move 语义和右值引用,确实使 C++ 更加“复杂”了,付出这个代价当然会有回报,STL 的性能得到了很大的提升,想想 vector<string> v; 这么一个对象,每当 v 的内存区域需要扩大或是缩小的时候,在没有 move 的时候,每个一存储在 v 中的 string 都需要重新拷贝一次,但现在不用了,它们只需要交换指针。而这一切你只需要换一个编译器就可以得到,完全不需要修改以前的程序。 当然,以后当我们再写会动态申请资源的类的时候(需要深拷贝的类(实现 the rule three)),如果实现上这个类的转移拷贝构造和赋值的函数的话,我们也可以在使用 STL 和其它一些函数的时候得这种性能上的好处。而且,它也使得“传值”在C++中变得不那么可怕了。你可以不再为了“性能”而不得不对程序的形式作出妥协,比如使用引用参数而不是返回值来得到函数的 output 等等。
作为一个“不怎么写模板和库”的程序员,我觉得理解了到这里为止的内容,就已经够了。而且这些内容并不容易消化。
完美转发,就留给下次有余力的时候再学习了~