Skip to content

15.4 std::move

By Alex on March 4th, 2017 | last modified by Alex on January 23rd, 2020

翻译by dashjay 2020.07.18

一旦你开始使用更加频繁的使用移动语义,你就会发现很多情况下,你想要调用移动语义,但是你创建的对象不得不作为左值,而不是右值。思考下列交换函数的代码:

#include <iostream>
#include <string>

template<class T>
void myswap(T& a, T& b)
{
  T tmp { a }; // invokes copy constructor
  a = b; // invokes copy assignment
  b = tmp; // invokes copy assignment
}

int main()
{
 std::string x{ "abc" };
 std::string y{ "de" };

 std::cout << "x: " << x << '\n';
 std::cout << "y: " << y << '\n';

 myswap(x, y);

 std::cout << "x: " << x << '\n';
 std::cout << "y: " << y << '\n';

 return 0;
}

传入两个 T(例子中使用了std::string) 类型的对象,这个函数交换他们的值通过三次拷贝。

x: abc
y: de
x: de
y: abc

在我们学习了上节课后,进行拷贝可能是低效的。并且这个版本的交换函数进行了三次拷贝。那会引起过量的字符串创建和销毁,是非常缓慢的。

然而,在这里进行拷贝是不必要的。我们真正要做的是交换 a 和 b 的值,可以被使用三次移动来替代!因此如果我们从拷贝语义切换到移动语义,我们可以使我们的代码拥有更高性能。

怎么做呢?这里的问题是 a 和 b 都是左值引用,不是右值引用,因此我们无法调用移动构造和移动赋值操作符来替代拷贝构造和拷贝赋值运算符。默认情况下,我们将会得到拷贝构造和拷贝赋值的行为。我们能怎么办呢?

std::move

在 C++11 中 std::move 是一个标准库函数,该函数只有一个目的,把参数转化为右值。我们可以传一个左值给 std::move,然后它就会返回一个右值引用。std::move 被定义在 utility 头中。

这有一个和上面相同,但是使用 std::move 将左值转化为右值,来实现 myswap(),因此我们可以调用移动语义:

#include <iostream>
#include <string>
#include <utility> // for std::move

template<class T>
void myswap(T& a, T& b)
{
  T tmp { std::move(a) }; // invokes move constructor
  a = std::move(b); // invokes move assignment
  b = std::move(tmp); // invokes move assignment
}

int main()
{
 std::string x{ "abc" };
 std::string y{ "de" };

 std::cout << "x: " << x << '\n';
 std::cout << "y: " << y << '\n';

 myswap(x, y);

 std::cout << "x: " << x << '\n';
 std::cout << "y: " << y << '\n';

 return 0;
}

This prints the same result as above:

x: abc
y: de
x: de
y: abc

但是这个例子中,性能高了许多。当 tmp 被初始化时,不再对 x 进行拷贝,我们用 std::move 来转化左值变量 x 成一个 r-value。因为参数是一个 右值,因此移动语义被触发,因此 x 被移动进 tmp

使用一对交换,x 的值被移动到了 y 上,并且 y 的值 已经被移动到了 x。

Another example

当用左值填充一个容器的元素时,我们也可以用 std::move ,例如 std::vector

在下面的程序中,我们先通过拷贝语义添加了一个元素到 vector,然后我们通过移动语义添加一个元素到 vector。

#include <iostream>
#include <string>
#include <utility> // for std::move
#include <vector>

int main()
{
 std::vector<std::string> v;
 std::string str = "Knock";

 std::cout << "Copying str\n";
 v.push_back(str); // calls l-value version of push_back, which copies str into the array element

 std::cout << "str: " << str << '\n';
 std::cout << "vector: " << v[0] << '\n';

 std::cout << "\nMoving str\n";

 v.push_back(std::move(str)); // calls r-value version of push_back, which moves str into the array element

 std::cout << "str: " << str << '\n';
 std::cout << "vector:" << v[0] << ' ' << v[1] << '\n';

 return 0;
}

程序打印:

Copying str
str: Knock
vector: Knock


Moving str
str:
vector: Knock Knock

在第一种情况下,我们传递了一个左值到 push_back() 中,因此它使用了拷贝语义来添加一个元素到 vecotr。因为这样,str 中的值被留下了。

在第二种情况下,我们传了一个右值到 push_back()(实际上是通过 std::move 将左值转化为右值),这样一来它使用移动语义来添加一个元素到 vector 这更加的高效,因为 vector 元素可以偷走 string 的值,而不是拷贝它。在这个情况下,str 会被留空。

在这个例子中,值得重申一遍的是 std::move() 提供了一个提示给编译器,程序员不再需要这个对象了(至少,不需要它当前的状态)。因此,你应该不要使用 std::move() 在任何持续使用,不想被修改的对象,并且你应该不要期望任何经过 std::move() 的对象在经过移动后相同!(you should not expect the state of any objects that have had std::move() applied to be the same after they are moved!)

移动函数应该总是保持对象 well-defined 的状态

如我们在之前的课程中了解的那样,最好总是让被窃取的对象保持某种良好的(确定性)的状态。在理想情况下,应该是一个 null state,将对象设置回它最初始或者0状态。现在我们可以谈谈为什么这样做:使用 std::move ,毕竟被窃取的对象也可能不是一个临时状态毕竟。用户可能想要再次重用这个(现在为空)的对象,或者测试它以某种方式,并且可能进行相应的计划(or test it in some way, and can plan accordingly)。

在以上的例子中,string 类型的 str 被设置成空字符串,在被移动之后(std::sttring 在一次成功的move后总是这么做)。这允许我们重用变量 str 如果我们希望(或者我们可以忽略它,如果我们不再需要使用它)。

std::move 在其他地方的应用

当排序一个数组时,std::move也可以很有用。许多排序算法(例如选择排序和冒泡排序)都会涉及一对元素的交换。在之前的课程中,我们已经借助拷贝语义来做交换,现在我们可以使用更高效的移动语义。

如果我们想要移动指针所管理的 contents 给另一个,也是很有用的。

结论

std::move 可能被用在每当我们需要对待一个左值像对待右值那样,为了实现移动语义而不是拷贝语义。