est tt = fun();
return 0;
}
当函数返回一个对象时,编译器会将这个对象看作的一个右值(准确来说是将亡值)。所以无需在fun函数中,将return t写成return std::move(t);
当然,实际上t变量的真身还是在fun函数中被摧毁了,但真身里面有价值的东西都被移走了。对!就像比克大魔王那样,临死前把自己的孩子留下来! 在C++里面,当然不能生成一个孩子,但是可以通过移动构造函数生成一个临时对象,把有价值的东西移走。因为不是移动到main函数的tt变量中,只是移动到了临时对象。所以接下来临时对象还要进行一次移动,把有价值的东西移动到main函数的tt变量中。这个移动过程无疑是一个很好的金蝉脱壳的经典教程。读者可以运行一下代码,可以看到整个移动过程。记住,用g++编译的时候要加入-fno-elide-constructors选项,禁止编译器使用RVO优化。因为这里的RVO优化比移动构造还是省力。所以如果不禁用,会优先使用RVO,而非移动构造函数。
初始化:
?
因为右值引用也是一个引用类型,所以只能初始化而不能赋值。既然这样,那只需讨论什么类型的值才能用于初始化一个右值引用。一般来说,右值引用只能引用右值、字面值、将亡值。所以问题转化为:什么是右值?网上介绍的一个方法是:要能不能将取地址符号&应用于某个标识符,如果能就说明它是一个左值,否则为右值。这个方法好像是行得通的。不过,我觉得没有必要分得那么清楚,又不是在考试。在平常写代码时,没有谁会写类似a+++++a这样的考试代码。我个人觉得,记住最常见的那几种就差不多了。比如,字面量(1,‘c'这类),临时(匿名)对象(即将亡值),经过std::move()转换的对象,函数返回值。其他的右值,还是留给编译器和Scott Meyers吧。如果真的要细究,可以参考stackoverflow上的一个提问《What are rvalues, lvalues, xvalues, glvalues, and prvalues?》
还有一个问题需要说明,const的左值引用(const T&)是一个万能引用,既可以引用左值,也能引用右值。这个是很特殊,特殊得很自然。如果Test类没有定义move构造函数,但用户又使用Test a = std::move(b)构造变量a。那么最终会调用Test类的copy构造函数。一个类的copy构造函数如果用户不定义,编译器会在必要情况下自动合成一个。所以上面的a变量肯定能构造。
谨慎的编译器:
?
前一段貌似隐隐约约说到编译器不会自动合成一个move构造函数。是的。如果用户定义了构造函数,copy构造函数,析构函数,operator =中的任何一个,编译器都不会自动为这个类合成一个move构成函数以及move 赋值函数,即使需要用到。我个人觉得是因为,当定义了那四个函数中的任何一个,都可以认为这个类不是nontrival的了。
想一下,在什么情况下我们是需要析构函数和copy构造函数的。当这个类里面有一些资源(变量)需要我们手动管理的时候。既然有资源要管理,那么读者你觉得编译器默认生成的move构造函数的内部实现应该是怎么样的呢?对类里面的所有成员都调用std::move进行移动?还是调用copy构造函数复制一份呢?这种吃力但又不见得讨好的事情,编译器选择不干。毕竟还有前面说到的const T& 可以引用一个右值。没有move构造函数,copy构造函数顶上即可。
作为类的设计者,你当然知道那些资源(变量)到底是move还是copy。如果是move的话,那么直接用=default告诉编译器:别担心,直接用对所有变量move就行了。如下:
?
class Test
{
public:
Test() p(new int) {}
~Test()=default;
Test(const Test&)=delete;
Test& operator = (const Test&)=delete;
Test(Test &&)=default;//告诉编译器
Test& operator = (Test &&)=default;//告诉编译器
private:
std::unique_ptr
p;
}
?