std::move
本节阅读量:一旦开始更频繁地使用移动语义,您会遇到这样的情况:希望调用移动语义,但手头要处理的对象是左值,而不是右值。以下面的交换函数为例:
|
|
传入两个T类型的对象(本例中为std::string)后,该函数通过制作三个副本来交换它们的值。因此,该程序打印:
|
|
正如上一课所示,复制可能效率低下。这个版本的交换会制作3个副本,导致大量不必要的字符串创建和销毁,因此速度较慢。
然而,这里并不需要复制。我们真正想做的只是交换a和b的值,而这也可以用3次移动完成!因此,如果从复制语义切换到移动语义,就可以让代码性能更好。
但该怎么做呢?这里的问题是,参数a和b是左值引用,而不是右值引用,因此无法直接调用移动构造函数和移动赋值运算符。默认情况下,会得到拷贝构造函数和拷贝赋值行为。该怎么办?
std::move
在C++11中,std::move是一个标准库函数,它会将其参数强制转换(使用static_cast)为右值引用,以便调用移动语义。因此,可以使用std::move将左值强制转换为更倾向于移动而不是复制的类型。std::move定义在utility头文件中。
下面是与上面相同的程序,但使用mySwapMove()函数,该函数使用std::move将左值转换为右值,以便我们可以调用移动语义:
|
|
这将打印与上面相同的结果:
|
|
在初始化tmp时,我们使用std::move将左值变量x转换为右值,而不是复制x。由于转换后是右值,因此会调用移动语义,并将x移动到tmp中。
经过后续几次移动,变量x的值被移动到y,而y的值被移动到x。
另一个例子
当使用左值填充容器元素(如std::vector)时,也可以使用std::move。
在下面的程序中,首先使用复制语义将元素添加到vector。然后,使用移动语义向vector添加元素。
|
|
在作者的机器上,该程序打印:
|
|
在第一种情况下,我们向push_back()传递了一个左值,因此它使用复制语义向vector添加元素。因此,str中的值被保留。
在第二种情况下,我们向push_back()传递了一个右值(实际上是通过std::move转换而来的左值),因此它使用移动语义将元素添加到vector。这更高效,因为可以窃取字符串的值,而不必复制它。
被移动的对象将处于有效但可能不确定的状态
当我们从临时对象中移动值时,被移动对象的剩余值并不重要,因为临时对象马上就会被销毁。但使用了std::move()的左值对象又如何呢?因为我们可以在移动这些对象的值后继续访问它们(例如,在上面的示例中,我们在移动str的值后打印它),所以确实可能访问到它们剩余的值。
这里有两种观点。一种观点认为,被移动的对象应该重置回某种默认/零状态,此时对象不再拥有资源。我们在上面看到了一个例子,其中str已被清空为空字符串。
另一种观点认为,我们应该采用最方便的做法;如果清空不方便,就不必强制要求被移动对象必须被清空。
那么标准库在这种情况下怎么做呢?关于这一点,C++标准规定:“除非另有规定,否则对象(C++标准库中定义的类型)被移动后应处于有效但未指定的状态。”
在上面的示例中,作者在调用std::move之后打印str的值时,得到的是空字符串。然而,这并不是强制要求,它可以打印任何有效字符串,包括空字符串、原始字符串或其他有效字符串。因此,我们应该避免使用被移动对象的值,因为结果取决于具体实现。
在某些情况下,我们希望重用值已经被移动的对象(而不是分配新对象)。例如,在上面的mySwapMove()实现中,我们先将资源移出,然后将另一个资源移入。这是可以的,因为在资源被移出和新值被赋入之间,我们从未使用a的值。
对于被移动对象,可以安全地调用任何不依赖对象当前值的函数。这意味着我们可以设置或重置被移动对象的值(使用operator=,或任何类型的clear()/reset()成员函数)。我们还可以测试被移动对象的状态(例如使用empty()查看对象是否有值)。然而,应该避免使用operator[]或front()之类的函数(它返回容器中的第一个元素),因为这些函数依赖容器中确实存在元素。
关键点
std::move()向编译器提供一个提示:程序员不再需要该对象当前的值。对象被移动后,仍可对其重新赋值。但在重新赋值之前,不要使用该对象的值。
std::move还有何用处?
在对元素数组进行排序时,std::move也很有用。许多排序算法(如选择排序和冒泡排序)都通过交换元素对来工作。在前面的课程中,我们不得不借助复制语义来交换。现在可以使用移动语义,这更高效。
如果我们想将一个智能指针管理的内容移动到另一个智能指针,它也很有用。
相关内容
std::move()的一个有用变体称为std::move_if_noexcept()。如果对象具有noexcept移动构造函数,则返回可移动的右值;否则返回可复制的左值。后续章节会介绍它。
结论
每当我们希望将左值视为右值时,都可以使用std::move,以调用移动语义而不是复制语义。