接【翻译】VC10中的C++0x新特性:右值引用(rvalue references) (1) move语义:移动左值现在,如果你喜欢使用赋值运算符来实现拷贝构造函数的话,将会怎么样?你可能会尝试使用你的move赋值运算符去实现move构造函数。这是可能的,但是你需要小心,下面就是一种错误的做法:
(编译器在这里实施了RVO优化,但没有NRVO。我上面已经提到,有些拷贝构造函数的调用被RVO和NRVO优化掉了,但编译器不是每次都能实施这种优化,move构造函数优化掉了剩余的情况) 在move构造函数里标记WRONG的那一行调用了copy赋值运算符,而不是move赋值运算符!这样也能正常编译运行,但move构造函数的目的没有达到。 为啥会这样呢?从C++98/03里回想一下,具名的左值引用是左值(int& r = *p;,r是左值)匿名的左值引用还是左值(vector<int>v(10, 1729),调用v[0]返回int&,这是个匿名左值引用,它的地址是可以获取的)。右值引用的行为就不同了:
具名的左值引用可以被重复的使用,也可以在上面施加多次操作。如果让具名左值引用成为右值的话,那施加在其身上的第一个操作可能就会把它的资源偷走,导致后续的操作失效。偷取是不应该影响其它操作的,因此具名左值引用应该是左值。另一方面,匿名的右值引用不会被重复使用,因此它可以保持自己的右值属性。 如果你真的想用move赋值运算符实现move构造函数,你需要一种把左值看作是右值的能力。C++0x头文件<utility>中的std::move()赋予你了这种能力,VC10会包含它。(译注:原文写作时,std::move()尚未包含进VC10,因此作者接着给出了std::move的实现)
(我将会把我实现的Move()作为std::move()来讲,因为它们实现原理是一样的)std::move()是如何工作的呢?目前,我只能告诉你这是个“牛×的魔法”。(一会儿我会详细解释,它并不复杂,但是它包括模版参数推导和引用退化(译注:引用的引用 = 引用),后面讲完美转发的时候还会遇到这俩东西)我可以用一个具体的例子来略过讲述这个魔法:给一个string类型的左值,就像上面代码中的up,std::move(up)调用的是 string&& std::move(string&),它返回的是个匿名的右值引用,而匿名的右值引用是右值。给定一个像上面代码中的strange()这样的右值string类型,std::move(strange())调用的是 string&& std::move(string&&),又一次,返回值是匿名右值引用,还是右值。 std::move在其它地方也非常有用。任何时候只要你拥有一个左值,而你已经不再需要它了(它将要被销毁或者被赋予别的值),你就可以使用std::move(左值表达式)来激活move语义。 move语义:可移动的数据成员C++0x的标准类(vector、string、regex等)都有move构造函数和move赋值运算符,并且我们已经了解了如何在自己的类中实现它们去手动的管理资源。但是当我们的类中有可move的数据成员(vector、string、regex)时咋弄?编译器不会为我们自动生成move构造函数和move赋值运算符。因此我们需要自己实现它们。幸运的是,有了std::move(),这就非常简单了:
就像你看到的,对逐个移动每个数据成员非常的繁琐。注意remote_point的move赋值运算符没有做自赋值检查因为remote_integer已经做了。还需要注意,remote_point隐式声明的拷贝构造函数、赋值运算符和析构函数都正确的完成了相应功能。 最后一个议题:你应该尽可能的为你的可拷贝的类实现move构造函数和move赋值运算符,因为编译器不会帮你生成它们。这样,不仅能在平时使用这些类时从move语义获得好处,STL容器和通用算法也可以得到move语义的好处,因为它们可以用移动代替昂贵的复制了。 转发的问题C++98/03中关于左值、右值、引用和模板的规则看起来非常完美。直到程序员尝试去写一些高度泛化的代码时,问题出现了。假设你要写一个完全泛化的函数outer(),它的目的是获取任意数目,任意类型的参数,然后将它们转发给函数inner()。已经有了一些不错的解决方案,比如工厂方法make_shared<T>(args)将args转发给T的构造函数,并且返回一个shared_ptr<T>。(这样就把T类型的对象和它的引用计数存储在了同一个内存块中,效率上和侵入式引用计数一样好)像function<Ret (Args)>这样的包装类,可以将参数传递给其内部存储的函数对象,等等。在本篇文章中,我们只对outer()将参数转发给inner()感兴趣。至于outer()的返回值类型如何推定,那是另外一个问题。(有时候很简单,比如make_shared<T>(args)总是返回shared_ptr<T>。但要完全解决这个问题,就需要用到C++0x的特性decltype了) 没有参数的情况就不讨论了,当参数为一个的情况时,让我们尝试去写一下outer():
这个outer()的问题是它不能转发右值性质的参数。如果inner()接受参数const int&,inner(5)没有问题,但outer(5)通不过编译,T会被推导为int,而int&不能绑定5。 好,让我们再来尝试:
这样,如果inner()接受参数为int&,那就违反了常量约束,因此通不过编译。 如果你可以分别针对T&和const T&重载,outer()确实能够工作,然后你就能够像使用inner()一样使用outer()。 不幸的是,当参数增多时,你需要非常繁琐的写一大堆重载函数:T1&和const T1&,T2&和const T2& ..等等。对于每个参数的增加,都会导致重载个数指数级的增加。(VC9 SP1里面的std::tr1::bind()函数非常牛×的为前5个参数做了这样的重载,包含了63个重载形式。否则的话,你就得给使用者解释为什么不能给函数对象绑定像1729这样的右值参数。为了产生出这些重载函数,需要使用令人想吐的预处理机制,恶心到你都不想去碰它) 转发的问题在C++98/03中是比较严重的,并且本质上无法解决(除了使用令人想吐的预处理机制,那会显著的降低编译速度,并且导致代码可读性超差无比)。但是,右值引用优雅的解决了这个转发问题。 (我已经在解释move语义模型之前解释了初始化和重载判定,但是现在我将先解说一下完美转发模型,然后再解释模板类型推导和引用退化规则。这样貌似更好一些) 完美转发:模型完美转发可以让你只写一个函数模板就能实现接受N个任意类型的参数并且将它们透明的转发给任意的函数。它们的常量/变量/左值/右值的属性都会得到保留,让你像使用inner()一样使用outer(),还可以和move语义一起工作而获得额外的好处(C++0x的可变长模板参数解决了“任意数目的参数”的问题,我们可以把N看做是任意数目)。猛一看可能有点神奇,但其实很简单:
太帅了!实现完美转发只需要写两行! 上面代码示范了怎样透明的将t1和t2转发给inner();inner()可以知道它们的左值性、右值性、常量性,就像它被直接调用一样。 就像std::move()一样,std::identity和std::forward()被定义在C++0x的<utility>头文件(VC10中会有);我已经演示了它们是如何实现的。(下面我将交替视使用std::identity和我自己实现的Identity,std::forward()和Forward,因为它们实现方法是相同的) 下面,让我们进行魔法揭秘吧。其实它依赖于模版参数推导和引用折叠技术。 |