C++11中的右值引用及move语义编程
C++0x中加入了右值引用,和move函数。右值引用出现之前我们只能用const引用来关联临时对象(右值)(造孽的VS可以用非const引用关联临时对象,请忽略VS),所以我们不能修临时对象的内容,右值引用的出现就让我们可以取得临时对象的控制权,终于可以修改临时对象了!而且书上说配合move函数,可以大大提高现有C++的效率。那么是怎样提高它的效率的呢?看段代码先!
#include <iostream> #include <utility> #include <vector> #include <string> int main() { std::string str = "Hello"; std::vector<std::string> v; // uses the push_back(const T&) overload, which means // we'll incur the cost of copying str v.push_back(str); std::cout << "After copy, str is \"" << str << "\"\n"; // uses the rvalue reference push_back(T&&) overload, // which means no strings will copied; instead, the contents // of str will be moved into the vector. This is less // expensive, but also means str might now be empty. v.push_back(std::move(str)); //注意: void push_back( T&& value ); std::cout << "After move, str is \"" << str << "\"\n"; std::cout << "The contents of the vector are \"" << v[0] << "\", \"" << v[1] << "\"\n"; }
结果:
After copy, str is "Hello" After move, str is "" The contents of the vector are "Hello", "Hello"
看完大概明白一点儿了,加上move之后,str对象里面的内容被"移动"到新的对象中并插入到数组之中了,同时str被清空了。这样一来省去了对象拷贝的过程。所以说在str对象不再使用的情况下,这种做法的效率更高一些!但问题是str的内容在什么地方被移走的呢?move函数到底是干啥的?扣一下stl源码吧,下面是move模板的源码:
// TEMPLATE FUNCTION move template<class _Ty> inline typename tr1::_Remove_reference<_Ty>::_Type&& move(_Ty&& _Arg) { // forward _Arg as movable return ((typename tr1::_Remove_reference<_Ty>::_Type&&)_Arg); //从这里到返回值的时候,发生了string的移动拷贝构造函数调用,故字符串转移到了右值引用变量中去了。 }
好吧,看过了这段,可能有人又迷惑了,不是说有名左指变量不能绑定到右值引用上面么?为什么move函数的参数是右值引用却可以接受左值变量作为参数?难道STL错了么?事实上,C++0x在引入右值引用的时候对函数模板自动推导也加入了新的规则,简单的说,像例子中的这种情况,模板参数是_Ty而函数的参数是_Ty&&(右值引用),同时_Arg是string的左值对象的情况下,会触发一个特殊规则,_Ty会推导成string&,也就是说此事推导出来的函数与move<string&>一致。那么move(_Ty&& _Arg) 得到的应该是move(string& && _Arg)这个时候根据引用折叠原则,会变成这个样子move(string& _Arg)。详细的描述参见白云飘飘翻译的vc技术文档(http://www.cppblog.com/kesalin/archive/2009/06/05/86851.html)。函数的返回值嘛,就好说了,就是返回所持有类型的右值引用了。所以,move函数的作用很简单,不管你给什么参数,都返回对应类型的右值引用!那么,上面例子中str的不是在move函数中被移走的。综上,我们猜测str内容肯定是在构造新对象的过程中被新对象偷走的,也就是在string的参数为右值引用的构造函数中被偷走的!翻看string的源码(来自VS实现的STL),果然如此啊!如下:
basic_string(_Myt&& _Right) : _Mybase(_STD forward<_Alloc>(_Right._Alval)) { // construct by moving _Right _Tidy(); assign(_STD forward<_Myt>(_Right)); } _Myt& assign(_Myt&& _Right) { // assign by moving _Right if (this == &_Right) ; else if (get_allocator() != _Right.get_allocator() && this->_BUF_SIZE <= _Right._Myres) *this = _Right; else { // not same, clear this and steal from _Right _Tidy(true); if (_Right._Myres < this->_BUF_SIZE) _Traits::move(this->_Bx._Buf, _Right._Bx._Buf, _Right._Mysize + 1); else { // copy pointer this->_Bx._Ptr = _Right._Bx._Ptr; _Right._Bx._Ptr = 0; } this->_Mysize = _Right._Mysize; this->_Myres = _Right._Myres; _Right._Tidy(); } return (*this); }
所以,我们知道了,C++0x在STL模板库中加入了参数为右值引用的构造函数,用于把参数所关联对象中的数据移动到新对象当中,避免了深度拷贝,增加了效率。再详细翻看源码,可以发现除了构造函数,operator=也重载了一个参数为右值引用的函数,用途和构造函数类似。所以我们自定义中的类也应该增加参数为右值引用的构造函数和重载赋值运算符!原因是啥,看例子!
未定义参数为右值引用的构造函数:
#include <iostream> #include <utility> #include <vector> #include <string> using namespace std; class MyPoint{ public: MyPoint() :comment(""), x(0), y(0) { } MyPoint(const MyPoint& p) :comment(p.comment),x(p.x),y(p.y) {} //MyPoint(MyPoint&& p) // :comment(move(p.comment)), x(p.x), y(p.y) //{ // p.x = 0; // p.y = 0; //} string toString() { char buf[100]; sprintf(buf, "%s: %d %d", comment.c_str(), x, y); return buf; } string comment; int x; int y; }; int main() { MyPoint p; p.comment = "First point"; p.x = 9; p.y = 7; vector<MyPoint> v; v.push_back(p); cout << "After copy, str is \"" << p.toString() << "\"\n"; v.push_back(move(p)); cout << "After move, str is \"" << p.toString() << "\"\n"; cout << "The contents of the vector are \"" << v[0].toString() << "\", \"" << v[1].toString() << "\"\n"; cin.get(); }
结果:
After copy, str is "First point: 9 7" After move, str is "First point: 9 7" The contents of the vector are "First point: 9 7", "First point: 9 7"
定义了参数为右值引用的构造函数之后:
After copy, str is "First point: 9 7" After move, str is ": 0 0" The contents of the vector are "First point: 9 7", "First point: 9 7"
综上所述,C++0x中的move语义编程,不仅仅是在应用的时候使用参数中加上move,对于自定义类需要增加参数为右值引用的构造函数和赋值运算符,这种构造函数我们称为move构造函数!