以前也看过一些 std::move
的介绍文章,总结下来一句话:std::move
没有任何代价。这个总结实在是坑人。
今天突然悟了,实际应用中悟的。如果写一个相关的教程,完全不应该从 std::move 入手,而是应该从如何降低对象拷贝代价入手。
struct A {
int m_a;
int m_b;
};
如果要对 A 做拷贝,可以写成下面这样。对于这种情况,就算用了 std::move,也没有什么用。
A a1;
g_a2 = a1; // g_a2 是一个类成员
但是,对于下面这个结构,就不一样了:
struct A {
int m_a;
int m_b;
char *m_s;
};
此时,下面的逻辑取决于 A 的拷贝构造函数实现,一般来说,会把 m_a
, m_b
, 直接赋值,m_s
则会做一个深拷贝。
A a1;
g_a2 = a1; // g_a2 是一个类成员
有些时候,我们不希望做深拷贝。浅拷贝行不行?一般来说是不行的。因为浅拷贝后 g_a2.m_s
和 a1.m_s
会指向同一片内存区域,并且 a1
是栈变量,它析构后会释放 m_s
指向的内存,那么 g_a2
就会得到一个野指针。
怎么办呢?C++ 发明了一种“移动”语义,让 A 实现下面这样一个移动拷贝函数:
struct A {
// 默认构造
A() : m_a(0), m_b(0.0), m_s(nullptr) {}
// 带参数构造
A(int a, int b, const std::string& s)
: m_a(a), m_b(b), m_s(new std::string(s)) {}
~A() { delete m_s; } // 释放内存
// 移动构造
A(A&& other) {
m_a = other.m_a;
m_b = other.m_b;
m_s = other.m_s;
other.m_s = nullptr; // 关键之处,强行删掉 other.m_s 的指针,实现了内存的“移动”
}
// 移动赋值
A& operator=(A&& other) {
if (this != &other) { // 防止自赋值
m_a = other.m_a;
m_b = other.m_b;
m_s = other.m_s;
other.m_s = nullptr; // 关键之处,强行删掉 other.m_s 的指针,实现了内存的“移动”
}
return *this;
}
};
A 还可以这样定义这个函数:
struct A {
void set(A&& other) {
m_a = other.m_a;
m_b = other.m_b;
m_s = other.m_s;
other.m_s = nullptr;
}
};
A a1;
g_a2.set(std::move(a1)); // g_a2 是一个类成员
所以,std::move 的确什么事情都不做,它只是提示编译器,去调用支持 move 语义的函数而已,具体做事情的,是这些函数。
Note:STL 中,vector 的 emplace_back、push_back 等也实现了支持 move 语义等版本。