前言
本文归纳总结了std::move的用法,使用场景,以及作用会对我们的程序带来哪些好处和使用时的注意事项。
一、std::move()使用场景
之前在右值引用和move语义总结中我们已经介绍过右值引用和移动语义,发现通过移动而不是拷贝会让我们的代码性能更高。但是移动有个要求是参数必须是右值,假设有如下代码:
#include <iostream>
#include <string>
template <typename T>
void mySwapCopy(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';
mySwapCopy(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
通过右值引用和move语义总结中,我们知道拷贝的性能差于移动,而且在这个例子中拷贝也是非必要的,因为我们只想交换a, b的值,完全没有必要拷贝一份,直接资源转移就可以。
所以通过移动可以让我们的代码更高效,但是问题是参数 a 和 b 是左值引用,而不是右值引用,因此我们没有办法调用 move 构造函数和 move 赋值运算符,调用的是 copy 构造函数和 copy 赋值。
1.std::move()
为了解决上面的问题,C++ 11引入了std::move(), std::move 是一个标准库函数,它将其参数(使用 static_cast)转换为右值引用,以便可以调用移动语义。因此,我们可以使用 std::move 将左值转换为右值。
提示:std::move在<utility>头文件中定义
使用std::move后原程序变为:
#include <iostream>
#include <string>
#include <utility> // for std::move
template <typename T>
void mySwapMove(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';
mySwapMove(x, y);
std::cout << "x: " << x << '\n';
std::cout << "y: " << y << '\n';
return 0;
}
输出结果跟之前一样,但是性能大幅提升。
2.std::move作用
在用左值填充容器的元素(例如 std::vector)时,我们也可以使用 std::move。
另一个例子:
#include <iostream>
#include <string>
#include <utility> // for std::move
#include <vector>
int main()
{
std::vector<std::string> v;
// We use std::string because it is movable (std::string_view is not)
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'; // The result of this is indeterminate
std::cout << "vector:" << v[0] << ' ' << v[1] << '\n';
return 0;
}
在我的编译器上输出结果如下:
Copying str
str: Knock
vector: Knock
Moving str
str:
vector: Knock Knock
在第一个push_back()中我们传入了左值,所以调用了拷贝构造函数;在第二个push_back()中我们使用std::move将左值转成了右值,调用的是移动语义,第二种方式性能更高。
二、注意事项
1.访问一个被move后的对象合法,但我们不应该对它的值有任何期待
当我们从临时对象中移动值时,被的对象留下什么值并不重要,因为临时对象无论如何都会立即被销毁。但是我们使用 std::move()的左值对象呢?因为我们可以在移动这些对象的值后继续访问它们(例如,在上面的例子中,我们在移动 str 的值后打印它),所以知道它们还剩下什么值是很有用的。
这里有两种思想流派。一种认为,已移出的对象应重置回某种默认/零状态,即对象不再拥有资源。我们在上面看到一个这样的例子,其中 str 已被清除为空字符串。
另外一种认为我们应该做任何最方便的事情,而不是局限于在不方便的情况下必须清除移出的对象。
提示:所以我们在移动后就不应该再依赖这个对象的值。
对于被移动后的对象,可以安全地调用任何不依赖于对象当前值的函数。这意味着我们可以设置或重置对象的值(使用 operator= 或任何类型的 clear()或 reset() 成员函数)。我们还可以测试 moved-from 对象的状态(例如,使用 empty() 查看对象是否有值)。但是,我们应该避免像 operator[] 或 front() 这样的函数(它们返回容器中的第一个元素),因为这些函数依赖于具有元素的容器,而被移动后的对象的值再重新赋值前任何值都有可能。
提示:std::move() 向编译器提示程序不再需要对象的值。不要对超出该点的对象值做出任何假设。在移动当前值后,可以给被移动的对象一个新值(例如使用 operator=)。
总结
本文介绍了使用std::move可以将左值转化为右值,从而扩展了移动构造和移动赋值运算符的使用场景,可以节约不必要的拷贝和销毁释放,让我们的代码更加高效。