我们知道移动语义是通过右值引用来匹配临时值的,那么,普通的左值是否也能借助移动语义来优化性能呢,那该怎么做呢?事实上C++11为了解决这个问题,提供了std::move方法来将左值转换为右值,从而方便应用移动语义。
move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存拷贝。深拷贝和move的区别如图2-1所示。
在图2-1中,对象SourceObject中有一个Source资源对象,如果是深拷贝,要将SourceObject拷贝到DestObject对象中,需要将Source拷贝到DestObject中;如果是move语义,要将SourceObject移动到DestObject中,只需要将Source资源的控制权从SourceObject转移到DestObject中,无须拷贝。
move实际上并不能移动任何东西,它唯一的功能是将一个左值强制转换为一个右值引用,使我们可以通过右值引用使用该值,以用于移动语义。强制转换为右值的目的是为了方便实现移动构造。
这种move语义是很有用的,比如一个对象中有一些指针资源或者动态数组,在对象的赋值或者拷贝时就不需要拷贝这些资源了。在C++11之前拷贝构造函数和赋值函数可能要像下面这样定义。假设一个A对象内部有一个资源m_ptr:
A& A::operator=(const A& rhs) { // 销毁m_ptr指向的资源 // 复制rhs.m_ptr所指的资源,并使m_ptr指向它 } 同样A的拷贝构造函数也是这样。假设这样来使用A: A foo(); // foo是一个返回值为X的函数 A a; a = foo();
最后一行将会发生如下操作:
销毁a所持有的资源。
复制foo返回的临时对象所拥有的资源。
销毁临时对象,释放其资源。
上面的过程是可行的,但是更有效率的办法是直接交换a和临时对象中的资源指针,然后让临时对象的析构函数去销毁a原来拥有的资源。换句话说,当赋值操作符的右边是右值的时候,我们希望赋值操作符被定义成下面这样:
A& A::operator=(const A&& rhs)
{
// 转移资源的控制权,无须复制
}
仅仅转移资源的所有者,将资源的拥有者改为被赋值者,这就是所谓的move语义。再看一个例子,假设一个临时容器很大,赋值给另一个容器。
{ std::list< std::string> tokens; // 省略初始化…… std::list< std::string> t = tokens; } std::list< std::string> tokens; std::list< std::string> t = std::move(tokens);
如果不用std::move,拷贝的代价很大,性能较低。使用move几乎没有任何代价,只是转换了资源的所有权。实际上是将左值变成右值引用,然后应用move语义调用构造函数,就避免了拷贝,提高了程序性能。当一个对象内部有较大的堆内存或者动态数组时,很有必要写move语义的拷贝构造函数和赋值函数,避免无谓的深拷贝,以提高性能。事实上,C++中所有的容器都实现了move语义,方便我们实现性能优化。
这里也要注意对move语义的误解,move只是转移了资源的控制权,本质上是将左值强制转换为右值引用,以用于move语义,避免含有资源的对象发生无谓的拷贝。move对于拥有形如对内存、文件句柄等资源的成员的对象有效。如果是一些基本类型,比如int和char[10]数组等,如果使用move,仍然会发生拷贝(因为没有对应的移动构造函数),所以说move对于含资源的对象来说更有意义。