M.4 - std::move
Key Takeaway
- 只有右值才能触发移动语义,因此必须是右值(字面量、匿名对象)或者被
std::move()
作用的左值才能够触发 std::move()
会提示编译器,程序员已经不再需要该值了。所以,std::move()
应该被应用于那些希望资源被移出的对象,且此后不应当假定该对象还持有该资源。给资源被移出的对象赋新值是可以的。
随着你开始频繁地使用移动语义,你会发现有时候在你需要使用它的时候,对象是左值而不是右值导致你无法使用。考虑下面这个swap
函数的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
传入两个T类型的对象(在这个例子中是std::string
)给函数,然后将它们的值交换(在这个过程中创建了三个副本)。程序输出结果为:
1 2 3 4 |
|
这个版本的swap
函数创建了三个副本, 而创建副本是低效的操作,这其中涉及到了多次字符串的创建和销毁,显然效率是很低的。
实际上,这里并不需要创建拷贝,因为我们执行希望将a和b的值交换,这个操作使用三次移动也可完成啊!所以如果能将拷贝语义替换为移动语义,代码无疑会更高效。
但是应该怎么做呢?这里的问题在于,a和b都是左值引用,而不是右值引用,所以没有办法调用移动构造函数和移动赋值运算符。默认情况下,基于左值引用执行的是拷贝构造函数和拷贝赋值操作符。那究竟应该如何实现呢?
std::move
在 C++11 中 标准库提供了std::move
函数用于将实参转换(使用静态类型转换)为右值引用,以便激活移动操作。因此,我们可以使用 std::move
对一个左值类型进行转换,使其能够被移动。std::move
定义在utility头文件中。
下面的程序和之前的类似,但是 myswapMove()
函数使用std::move
函数将左值转换成了右值,所以调用了移动语义相关操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
程序输出:
1 2 3 4 |
|
程序的输出结果一样,但是效率要高得多。当tmp
初始化时,它没有创建x副本,这里我们使用 std::move
将左值x转换成了一个右值。因为参数是右值,所以调用了移动构造函数,x被移动到了tmp
。
经过几次转换,变量x的值被移动到了y,而y的值也被移动到了x。
另外一个例子
我们还可以使用std::move
来填充容器的元素,例如使用左值填充 std::vector
。
在下面的例子中,我们首先使用拷贝语义向vector
添加元素。然后使用移动语义做同样的操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
在笔者的机器上会输出如下信息:
1 2 3 4 5 6 7 |
|
在第一种情况下,传入 push_back()
的是一个左值,所以它会使用拷贝语义向vector
添加元素。因此,原字符串还被保存在str
中。
对于第二种情况,传入 push_back()
的是一个右值(准确来讲是通过std::move
将左值转换成的右值),所以它会使用移动语义向vector
添加元素。这个操作无疑是更加高效的,因为vector
的元素“窃取了”字符串的值,而没有创建它的拷贝。
资源被移动的对象处于一种有效,但可能不确定的状态
当我们从一个临时对象中把资源移动出来时,这个临时对象中还剩下什么其实并不重要,因为它马上就要被销毁了。但是对于一个左值对象来说,当我们对它使用std::move()
后会怎么样呢?因为该左值在其资源被移动后,仍然可能会被访问(在这个例子中,当我们打印str
的值时,内容已经没有了),所以有必要知道该对象资源被移动后,还有什么留在原对象中。
关于这个问题的思考有两个派系。一派人任务,资源已经被移动走的对象,应该被重置为某种默认状态或0幢,因为它不再拥有资源。上面的例子很好地展示了这种情况。
另外一派人则认为应该“怎么方便怎么来”,如果清理该对象很麻烦,那就不必清理。
那么,标准库是如何做的呢?关于这个问题,C++标准认为:“除非有明确的规定,否则C++标准库中定义的类型,在资源被移动后应该被置于任意一种有效的状态”。
在上面的例子中,当左值打印 str
值时,程序输出的空字符串。但是,这并不是一定的,打印任何其他合法的字符串也可以,包括空字符串、原本的字符串或任何合法的字符串。因此,我们应该避免继续使用已经被移动的左值对象,因为其结果取决于编译器的具体实现。
在有些场景下,我们需要继续使用已经被移动的对象(而不希望重新分配一个新对象)。例如,在上面 myswapMove()
的实现中,我们首先将a的资源移出,然后将其他资源移动到a。这么做可行是因为,a的值被移除到a再次获得一个确定的值的这段时间内,我们不会使用a的值。
对于一个被移动了资源的对象来说,调用任何不依赖其值的函数是安全的。也就是说,我们可以设置、重置该对象的值(使用=
或clear()
或reset()
成员函数)。我们可以检查它的状态(例如使用 empty()
判断该对象是否有值)。但是,我们必须避免operator[]
或 front()
这样的函数,因为这些函数依赖于对象中存放的值,而被移动后的对象可能可以,也可能不能够提供这些值。
关键信息
std::move()
会提示编译器,程序员已经不再需要该值了。所以,std::move()
应该被应用于那些希望资源被移出的对象,且此后不应当假定该对象还持有该资源。给资源被移出的对象赋新值是可以的。
std::move
还有什么用途?
std::move
在进行数组排序时也很有用。很多排序算法 (例如选择排序和冒泡排序)是基于交换两个元素而实现的。在之前的课程中,我们使用了基于拷贝语义的交换,现在可以将它替换成基于移动语义的交换了,这么做效率会高很多。
此外,当我们需要将某个智能指针管理的内容转移到另一个智能指针时,该函数也很有用。
小结
当一个左值需要被当做右值使用,以便触发移动语义时,可以使用std::move
。