纠正一下对cpp移动语义的错误理解

之前对于移动语义的理解就是使用std::move将一个对象所占有的资源的所有权转移给另一个对象,但是只要使用std::move就足够了吗?这显然是错误的。

看一下std::move的源码(g++12.2)

1
2
3
4
5
6
7
8
9
10
/**
* @brief Convert a value to an rvalue.
* @param __t A thing of arbitrary type.
* @return The parameter cast to an rvalue-reference to allow moving it.
*/
template<typename _Tp>
_GLIBCXX_NODISCARD
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
{ return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }

其实move的实现并没有很复杂,粗略一点的理解就是将一个左值强制转换为右值。

std::move 并不会真正地移动对象,真正的移动操作是在移动构造函数、移动赋值函数等完成的,std::move 只是将参数转换为右值引用而已。

写一个简单的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <fmt/core.h>
#include <string>
#include <utility>

struct A {
A(std::string str)
: data(str) {}
A(const A&) {
puts("copy");
}
A(A&&) {
puts("move");
}

std::string data = "default";
};

int main() {
A a("hello");
A a2(std::move(a));
fmt::print("a: {}, a2: {}\n", a.data, a2.data);
}

看样子我们使用std::move后a2的data数据应该为“hello”,但是运行结果为

1
2
move
a: hello, a2: default

虽然使用move匹配到了A的移动构造函数,但是在上文提到过,std::move仅仅只是一个强制转换,并没有实现真正的移动!但要是我们不写A中的移动构造函数或是将其设置成default

1
2
3
4
5
6
7
8
9
10
struct A {
A(std::string str)
: data(str) {}
A(const A&) {
puts("copy");
}
A(A&&) = default;

std::string data = "default";
};

这样,运行结果为

1
a: , a2: hello

这是因为当我们不显式指定移动构造函数(或是拷贝构造函数、移动or拷贝运算符)编译器会自动生成,貌似也一并实现了数据的移动(?这我也还不清楚)

我们通常使用std::move能够实现标准库中一些资源的转移,是因为标准库中已经实现了这些资源类的移动构造函数or移动赋值运算符。