章节目录

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
#include <iostream>
#include <string>

template <typename T>
void mySwapCopy(T& a, T& b) 
{ 
	T tmp { a }; // 调用拷贝构造
	a = b; // 调用拷贝赋值
	b = tmp; // 调用拷贝赋值
}

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

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

	mySwapCopy(x, y);

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

	return 0;
}

传入两个T类型的对象(本例中为std::string)后,该函数通过制作三个副本来交换它们的值。因此,该程序打印:

1
2
3
4
x: abc
y: de
x: de
y: abc

正如上一课所示,复制可能效率低下。这个版本的交换会制作3个副本,导致大量不必要的字符串创建和销毁,因此速度较慢。

然而,这里并不需要复制。我们真正想做的只是交换a和b的值,而这也可以用3次移动完成!因此,如果从复制语义切换到移动语义,就可以让代码性能更好。

但该怎么做呢?这里的问题是,参数a和b是左值引用,而不是右值引用,因此无法直接调用移动构造函数和移动赋值运算符。默认情况下,会得到拷贝构造函数和拷贝赋值行为。该怎么办?


std::move

在C++11中,std::move是一个标准库函数,它会将其参数强制转换(使用static_cast)为右值引用,以便调用移动语义。因此,可以使用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
#include <iostream>
#include <string>
#include <utility> // for std::move

template <typename T>
void mySwapMove(T& a, T& b) 
{ 
	T tmp { std::move(a) }; // 调用移动构造
	a = std::move(b); // 调用移动赋值
	b = std::move(tmp); // 调用移动赋值
}

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

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

	mySwapMove(x, y);

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

	return 0;
}

这将打印与上面相同的结果:

1
2
3
4
x: abc
y: de
x: de
y: abc

在初始化tmp时,我们使用std::move将左值变量x转换为右值,而不是复制x。由于转换后是右值,因此会调用移动语义,并将x移动到tmp中。

经过后续几次移动,变量x的值被移动到y,而y的值被移动到x。


另一个例子

当使用左值填充容器元素(如std::vector)时,也可以使用std::move。

在下面的程序中,首先使用复制语义将元素添加到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
#include <iostream>
#include <string>
#include <utility> // for std::move
#include <vector>

int main()
{
	std::vector<std::string> v;

	// 使用 std::string 因为它是可移动的 (std::string_view 不是)
	std::string str { "Knock" };

	std::cout << "Copying str\n";
	v.push_back(str); // 调用左值版本的 push_back, 拷贝 str到容器
	
	std::cout << "str: " << str << '\n';
	std::cout << "vector: " << v[0] << '\n';

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

	v.push_back(std::move(str)); // 调用右值版本 push_back, 移动 str 到容器
	
	std::cout << "str: " << str << '\n'; // 这一行结果不确定,因为str的内容被move走了
	std::cout << "vector:" << v[0] << ' ' << v[1] << '\n';

	return 0;
}

在作者的机器上,该程序打印:

1
2
3
4
5
6
7
Copying str
str: Knock
vector: Knock

Moving str
str:
vector: Knock Knock

在第一种情况下,我们向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,以调用移动语义而不是复制语义。


22.2 移动构造函数和移动赋值函数

上一节

22.4 std::unique_ptr

下一节