在C++17之前,std::vector 的移动构造和移动赋值仅在“非搬迁” (no‑except) 时才会避免复制元素。随着 C++17 引入了更智能的异常安全机制,标准库实现开始区分 “no‑except” 移动构造函数和可能抛异常的情况。下面我们拆解这一细节,看看在什么情况下会出现看似“多余”的复制,以及如何通过显式控制来避免它。
1. std::vector 的搬迁与异常安全
1.1 基本移动语义
std::vector <int> a{1,2,3};
std::vector <int> b = std::move(a);
此时,b 取得 a 的内部缓冲区指针,a 变为空向量。若 int 的移动构造没有抛异常,整个过程不涉及元素复制。
1.2 何时会复制?
移动构造函数的实现通常是:
vector(vector&& other) noexcept : data(other.data), size(other.size), capacity(other.capacity) {
other.data = nullptr;
other.size = 0;
other.capacity = 0;
}
但 noexcept 关键字是根据元素类型的移动构造是否 noexcept 自动推导的。若 T 的移动构造可能抛异常,编译器就不标记 vector 的移动构造为 noexcept,于是标准库会回退到 强异常安全 模式:
- 为目标
vector 重新分配足够的空间;
- 逐个移动构造元素,若在任何一次抛异常,已完成的构造会被析构;
- 若所有移动成功,
vector 则把内部指针指向新空间。
这一步骤正是我们看到的“额外复制”——实际上是移动构造(不是复制)。
1.3 何时不抛异常?
标准规定,对于基础类型(int, double 等)以及满足 noexcept 的移动构造函数,移动不会抛异常。也就是说,如果你自定义的类型 T 的移动构造被声明为 noexcept,`vector
` 的移动构造也会成为 `noexcept`,从而避免额外的复制。
—
## 2. 典型场景:自定义类中的抛异常
考虑下面的类:
“`cpp
class Blob {
public:
Blob(std::string data) : data_(std::move(data)) {}
// 移动构造会抛异常(例如当 data_ 为空时)
Blob(Blob&& other) noexcept(false)
: data_(std::move(other.data_)) {
if (data_.empty()) throw std::runtime_error(“Empty Blob”);
}
// 赋值与移动赋值省略
private:
std::string data_;
};
“`
`Blob` 的移动构造显式声明为 `noexcept(false)`,这导致:
“`cpp
std::vector
vec1{Blob(“hello”), Blob(“world”)};
std::vector
vec2 = std::move(vec1);
“`
在移动 `vec1` 到 `vec2` 时,标准库会先为 `vec2` 重新分配空间,然后逐个调用 `Blob` 的移动构造。如果某个 `Blob` 抛异常,已成功移动的元素会被析构,而 `vec2` 仍保持原始状态。为了保证这一过程的强异常安全,额外的空间分配与元素复制不可避免。
—
## 3. 如何避免额外复制
### 3.1 声明 `noexcept` 的移动构造
如果你确定 `T` 的移动构造在任何情况下都不会抛异常,可以使用:
“`cpp
Blob(Blob&& other) noexcept : data_(std::move(other.data_)) {}
“`
此时 `std::vector
` 的移动构造会被标记为 `noexcept`,从而直接搬迁指针。
### 3.2 使用 `std::move_if_noexcept`
在需要将容器移动到另一个容器但又不想手动检查 `noexcept` 时,可以用 `std::move_if_noexcept`:
“`cpp
std::vector
vec2;
vec2.reserve(vec1.size());
std::copy(std::make_move_iterator(vec1.begin()),
std::make_move_iterator(vec1.end()),
std::back_inserter(vec2));
“`
若 `Blob` 的移动构造是 `noexcept`,`move_if_noexcept` 会直接移动;否则会退回到复制。
### 3.3 直接分配与构造
如果你自己手动控制 `vector` 的分配和构造,可以避免标准库的搬迁逻辑:
“`cpp
std::vector
vec2;
vec2.reserve(vec1.size());
for (auto& item : vec1) {
vec2.emplace_back(std::move(item)); // 直接移动构造
}
“`
这里使用 `emplace_back` 明确告诉编译器你要移动构造元素,而不是复制。
—
## 4. 小结
– **移动构造抛异常** 会导致 `std::vector` 在移动时额外分配内存并逐个移动元素,以保证强异常安全。
– 通过 **`noexcept` 声明** 或 `std::move_if_noexcept` 可以让 `std::vector` 直接搬迁指针,从而避免额外复制。
– 在性能敏感的场景中,最好保证自定义类型的移动构造是 `noexcept`,或者手动使用 `emplace_back` 与 `reserve` 来精细控制。
了解这些细节可以帮助你在写高性能 C++ 代码时避免不必要的复制开销,同时保持异常安全。