右值引用的功能
首先,我并不介绍什么是右值引用,而是以一个例子里来介绍一下右值引用的功能:
12345678910111213141516171819202122232425 | #include <iostream> #include <vector> using namespace std; class obj { public : obj() { cout << ">> create obj " << endl; } obj(const obj& other) { cout << ">> copy create obj " << endl; } }; vector<obj> foo() { vector<obj> c; c.push_back(obj()); cout << "---- exit foo ----" << endl; return c; } int main() { vector<obj> k; k = foo(); } |
首先我们编译一下这个函数,运行结果如下:
1234567 | tianfang > g++ main.cpp tianfang > a.out >> create obj >> copy create obj ---- exit foo ---- >> copy create obj tianfang > |
可以看到,对obj对象执行了两次构造。vector是一个常用的容器了,我们可以很容易的分析这这两次拷贝构造的时机:
由于对象的拷贝构造的开销是非常大的,因此我们想就可能避免他们。其中,第一次拷贝构造是vector的特性所决定的,不可避免。但第二次拷贝构造,在C++ 11中就是可以避免的了。
123456 | tianfang > g++ -std=c++11 main.cpp tianfang > a.out >> create obj >> copy create obj ---- exit foo ---- tianfang > |
可以看到,我们除了加上了一个-std=c++11选项外,什么都没干,但现在就把第二次的拷贝构造给去掉了。它是如何实现这一过程的呢?
在老版本中,当我们执行第二行的赋值操作的时候,执行过程如下:
在C++11的版本中,执行过程如下:
关键的过程就是第2步,它不是复制而是交换,从而避免的成员的拷贝,但效果却是一样的。不用修改代码,性能却得到了提升,对于程序员来说就是一份免费的午餐。
但是,这份免费的午餐也不是无条件就可以获取的,带上-std=c++11编译时,如果使用STL代码可以享用这份午餐,但如果使用我们以前的老代码发现还是和以前的功能是一样的,那么,如何让我们以前的代码也能得到这个效率的提升呢?
通过交换减少数据的拷贝
为了演示如何在我们的代码中也获取这个性能提升,首先我先写了一个山寨的vector:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354 | #include <iostream> #include <vector> using namespace std; class obj { public : obj() { cout << ">> create obj " << endl; } obj(const obj& other) { cout << ">> copy create obj " << endl; } }; template <class T> class container { public: T* value; public: container() : value(NULL) {}; ~container() { delete value; } container(const container& other) { value = new T(*other.value); } const container& operator = (const container& other) { delete value; value = new T(*other.value); return *this; } void push_back(const T& item) { delete value; value = new T(item); } }; container<obj> foo() { container<obj> c; c.push_back(obj()); cout << "---- exit foo ----" << endl; return c; } int main() { container<obj> k ; k = foo(); } |
这个vector只能容纳一个元素,但并不妨碍我们的演示,其功能和前面的例子是一样的,运行这段代码,结果如下:
12345678 | tianfang > make g++ -std=c++11 main.cpp tianfang > a.out >> create obj >> copy create obj ---- exit foo ---- >> copy create obj tianfang > |
如前所述,仍然有两次拷贝构造。其实前面已经说过交换实现减少拷贝构造的原理,那么,我们可以通过修改 ‘=’ 函数来手动实现这一过程。
1234567 | const container& operator = (container& other) { T* tmp = value; value = other.value; other.value = tmp; return *this; } |
在VC中运行这段代码,发现运行结果和预期一致,
123 | >> create obj >> copy create obj ---- exit foo ---- |
但是,gcc中却无法通过编译,原因很简单:gcc期望的赋值函数的参数是const型的,而这里为了交换成员,而不能使用const型。
那么,虽然gcc中不能生效,是否可以说在vc中就可以以这种形式获取性能提升呢?答案是否定的。虽然在这段代码中这么写没有问题,但赋值函数本身是期望复制功能的,而不是交换。例如,修改后下面的运行结果就不对了。
12345678 | int main() { container<obj> k, k2; k = foo(); //预期结果是复制,但执行了交换 k2 = k; } |
gcc的告警是有道理的:如果 ‘=’ 函数实现的是复制功能,虽然效率低点,但保证了功能正确,但如果实现的是交换的功能,则不能保证功能一定正确。只有当 ‘=’ 函数右边的对象为一个临时变量的时候,由于临时变量会马上被删除掉,此时的交换和复制的效果是一样的。其实VC也应该把这个告警加上才合适。
PS:对临时变量定义和来源不清楚的朋友可以参考一下这篇。
现在的问题是:我们无法在赋值函数里区分传入的是一个临时对象还是非临时对象,因此只能执行复制操作。为了解决这一问题,c++中引入了一个新的赋值函数的重载形式:
1 | container& operator = (container&& other) |
这个赋值函数通常称为移动赋值函数,和老版本的相比,它有两点区别:
现在,我们就有两个版本的赋值函数了,C++11在语法级别也做了适应:
现在,我们实现一下山寨版的移动赋值函数:
12345678 | container& operator = (container&& other) { delete value; value = other.value; other.value = NULL; return *this; } |
和移动赋值函数相应的,也有一个一个移动构造函数,也最好实现以下:
12345 | container (container&& other) { value = other.value; other.value = NULL; } |
我们也可以实现自己的右值引用版的重载函数,这里就不多介绍了。
注意:本文所示的代码只是为了演示和实现右值引用,力求简洁,并没有写得很完善(一个典型的缺失就是在赋值函数中没有判断入参是否是本身),请不要将其应用于项目中。
完善的版本请看MSDN文章:,其相应的对右值引用的介绍文章也非常值得一读。
通过std::move函数显式使用交换
首先看一下这段代码:
1234567891011121314151617 | class bigobj { public : bigobj() { cout << ">> create obj " << endl; } bigobj(const bigobj& other) { cout << ">> copy create obj " << endl; } bigobj(bigobj&& other) { cout << ">> move create obj " << endl; } }; int main() { list<bigobj> list; for(int i = 0; i < 3; i++) { bigobj obj; list.push_back(obj); } } |
运行的时候就会发现:虽然我们定义了移动构造函数,但是它仍然会执行拷贝构造函数。这是因为编译器并不认为obj是临时变量。关于什么变量才是临时变量,前文已经给了个来说明它,简单的说,我们能够看到的命名变量都不是临时变量。
虽然obj对象不是语言级别的临时变量,但是从功能上来看,它就是一个临时变量,是可以使用移动构造函数来消除拷贝带来的性能损失的。为了解决这一问题,C++提供了一个move函数来把obj变量强制转换为右值引用,这样就可以使用移动构造函数了。
12345 | for(int i = 0; i < 3; i++) { bigobj obj; list.push_back(std::move(obj)); } |
不过,需要注意的是,和系统识别的临时变量而自动使用右值引用不同,这种强制转换是有一定的风险的,由于在push_back后执行了交换操作,如果再次使用它会出现非预期的结果,只有能确定该变量不会再次被使用才能执行这种转换。
来源: http://blog.jobbole.com/108685/