移动语义
英文: Move Semantics.
标准: C++11.
移动语义是 C++11 引入的特性, 旨在减少不必要的拷贝.
与移动语义一起被引入的概念还有右值引用 (rvalue references) 和移动构造函数 (move constructor), 但自上而下的理解该概念的方法是了解何为移动语义, 而非何为右值引用. 右值引用只是 C++ 用来实现移动语义的具体实现方法.
为什么需要移动语义?
以下面的字符串类型为例:
class String {
public:
String(const char* str) {
buf_ = new char[strlen(str) + 1];
strcpy(buf_, str);
}
// 拷贝构造函数 (深拷贝)
String(const String& other) : String(other.buf_) {}
virtual ~String() { delete[] buf_; }
private:
char* buf_;
};
由于 C++ 默认的拷贝类型为浅拷贝, 因此上面例子为手动实现了进行深拷贝的拷贝函数.
可以看出, 在进行深拷贝时, 需要为字符串分配新的内存, 并将原字符串的内容拷贝到新分配的内存中. 这意味着拷贝 String
有一定的开销.
如果要将 String
放入容器中, 比如 std::vector<String>
:
std::vector<String> strings;
{
String str("hello");
// 调用 `push_back(const T&)` 和 `String(const String&)`
strings.push_back(str);
}
我们将变量 str
存入容器中, 以便后续通过容器访问他们. str
变量本身在这之后就不需要使用了.
移动语义
在 C++11 之前, 只有引用传递和值传递两种方式:
- 值传递: 在传参时就会进行深拷贝.
- 引用传递: 由于容器保存的是
String
, 而非&String
, 所以push_back(const T&)
函数内部依然需要进行深拷贝.
在 C++11 之后, 引入了移动语义, 包括所谓的右值引用和移动构造函数:
class String {
public:
// ... SKIP ...
// 参数类型为右值引用
String(String&& other) noexcept : buf_(other.buf_) {
// 防止双重释放
other.buf_ = nullptr;
}
// ... SKIP ...
};
上面代码在原本 String
的基础上添加了移动构造函数, 其参数是一个右值引用.
其中 other
是被移动的变量, 移动后便不应该再被使用. 因此即使其行为与浅拷贝相似, 但依然合法.
其中新的 String
直接接管了旧 String
的 buf_
, 没有进行任何的动态内存分配和字符串拷贝操作.
为了调用移动构造函数, 避免不必要的拷贝, 需要先通过 std::move()
函数将 str
从左值转为右值引用. 这样就会通过函数重载, 调用接受右值引用参数的移动构造函数 (即 push_back(T&&)
).
其中 std::move()
可以简单的视作 static_cast<T&&>(t)
, 即将 t
转为右值引用.
使用了移动语义的代码如下:
std::vector<String> strings;
{
String str("hello");
// 调用 `push_back(T&& value)` 和 `String(String&& other)`
strings.push_back(std::move(str));
// `str` 已经被移动, 不应该继续使用.
// 生命周期结束后将自动调用析构函数,
// 由于移动后 `str.buf_` 被赋值为 `nullptr`, 所以后续可以安全的执行 `delete buf_`
}
原位构造 (in-place construction)
针对上述问题, C++11 还给出了一种解决方案, 虽然不能完全替代移动语义, 但是在部分情况下, 甚至比使用移动语义更高效, 因为无需调用移动构造函数.
就是 std::vector<T,Allocator>::emplace_back()
这类函数, 它会直接在容器中构造元素, 无需进行拷贝或移动操作.
不过这已经超出了移动语义的讨论范围, 因此这里不再赘述.