文章内容:
在C++11 之后,移动语义成为了提高性能和资源利用率的核心技术之一。尤其是在实现自定义容器(如 Vector、List 或 Stack)时,合理使用移动构造函数和移动赋值运算符可以显著减少不必要的拷贝开销。下面以一个简易的 Vector 容器为例,演示如何为其添加移动语义,并说明关键点与常见陷阱。
1. 先定一个基本的 Vector 结构
template <typename T>
class SimpleVector {
public:
SimpleVector() : data_(nullptr), size_(0), capacity_(0) {}
~SimpleVector() { delete[] data_; }
void push_back(const T& value) { // 拷贝插入
if (size_ == capacity_) reserve(capacity_ == 0 ? 1 : capacity_ * 2);
new (data_ + size_) T(value);
++size_;
}
size_t size() const noexcept { return size_; }
private:
void reserve(size_t new_cap) {
T* new_data = new T[new_cap];
for (size_t i = 0; i < size_; ++i)
new (new_data + i) T(std::move(data_[i])); // 这里可以使用移动构造
clear(); // 调用析构
delete[] data_;
data_ = new_data;
capacity_ = new_cap;
}
void clear() {
for (size_t i = 0; i < size_; ++i)
data_[i].~T();
size_ = 0;
}
T* data_;
size_t size_;
size_t capacity_;
};
上面仅提供了一个非常基础的实现,未考虑移动语义。
下面我们逐步引入移动构造函数、移动赋值运算符以及移动插入(push_back)的重载。
2. 加入移动构造函数
移动构造函数应当把资源“转移”到新对象,同时将原对象置为安全的空状态。
SimpleVector(SimpleVector&& other) noexcept
: data_(other.data_), size_(other.size_), capacity_(other.capacity_) {
other.data_ = nullptr;
other.size_ = 0;
other.capacity_ = 0;
}
noexcept是必需的,移动构造函数不应抛异常,否则在容器内部使用移动构造时会导致std::terminate。- 将原对象的指针、大小和容量全部转移,并把原对象置为一个安全的空状态。
3. 加入移动赋值运算符
移动赋值运算符需要先释放自己的资源,然后“借用”右值对象的资源。
SimpleVector& operator=(SimpleVector&& other) noexcept {
if (this != &other) {
clear();
delete[] data_;
data_ = other.data_;
size_ = other.size_;
capacity_ = other.capacity_;
other.data_ = nullptr;
other.size_ = 0;
other.capacity_ = 0;
}
return *this;
}
注意:先调用 clear(),释放元素的析构,然后再删除内存;随后转移资源。
4. 推进 push_back 以支持移动
重载 push_back,让它接受一个右值引用。
void push_back(T&& value) { // 移动插入
if (size_ == capacity_) reserve(capacity_ == 0 ? 1 : capacity_ * 2);
new (data_ + size_) T(std::move(value));
++size_;
}
此重载与之前的 push_back(const T&) 并存,编译器会根据实参的值类别自动选择。
5. 关键注意点
| 位置 | 说明 |
|---|---|
| 析构 | 只销毁 size_ 个元素,避免销毁未初始化的内存。 |
| 拷贝构造 / 赋值 | 如果不打算支持拷贝,直接删除(= delete),或者按需实现。 |
| 异常安全 | reserve 中使用 new 与 std::move,若 T 的移动构造抛异常,已有元素已被移动,资源仍安全。 |
noexcept |
移动构造函数和移动赋值运算符必须标记 noexcept,否则容器内部的 move_if_noexcept 机制会退回拷贝。 |
| 容量管理 | 重新分配时最好先移动元素再销毁旧内存,减少构造/析构次数。 |
6. 简单测试
#include <iostream>
#include <string>
int main() {
SimpleVector<std::string> vec;
vec.push_back("Hello");
vec.push_back("World");
vec.push_back(std::string("C++")); // 自动调用移动重载
SimpleVector<std::string> vec2 = std::move(vec); // 移动构造
std::cout << "vec size after move: " << vec.size() << '\n';
std::cout << "vec2 size: " << vec2.size() << '\n';
}
输出示例(取决于编译器实现):
vec size after move: 0
vec2 size: 3
7. 小结
- 移动语义 能显著降低自定义容器在大规模数据搬迁时的开销。
- 正确实现
移动构造函数、移动赋值运算符和移动插入,并在关键地方标记noexcept,是实现高性能容器的基础。 - 通过
reserve、clear与std::move的配合,保证了异常安全与资源完整性。
参考上述示例,你可以在自己的项目中轻松为任何需要的自定义容器添加移动语义,从而提升整体性能与代码质量。