Appearance
vector 扩容:到底是拷贝还是移动?
这是 STL 里最容易被"背答案"背错的高频考点。很多人答"扩容就是把元素 move 过去",不全对——标准规定:扩容时旧元素是移动还是拷贝,由
std::move_if_noexcept决定。
一、结论先行(TL;DR)
vector 触发 reallocation(容量不足、需要搬迁到新内存)时,旧元素的搬迁方式:
| 条件 | 搬迁方式 |
|---|---|
T 的移动构造函数是 noexcept(is_nothrow_move_constructible_v<T> 为真) | 移动 |
T 不可拷贝(is_copy_constructible_v<T> 为假) | 移动(异常后果 unspecified) |
| 其余情况(可拷贝 且 移动可能抛异常) | 拷贝 |
T 是平凡可拷贝(trivially copyable) | 位块搬移(memmove/memcpy),既不调拷贝也不调移动 |
一句话记忆:"移动安全(noexcept)就移动,不安全但有拷贝就用拷贝,连拷贝都没有才硬移动"。这正是 std::move_if_noexcept 的语义。
二、决策流程(先判什么,再判什么)
判定优先级:① 能不能位块搬(平凡)→ ② 移动安不安全(noexcept)→ ③ 有没有拷贝兜底。
text
扩容搬迁旧元素
│
├─ Trivially Copyable? ── 是 ─▶ memmove 位块搬移 🚀 极快
└─ 否
├─ move 是 noexcept? ── 是 ─▶ 移动构造 🛵 良好
└─ 否
├─ 可拷贝? ── 是 ─▶ 拷贝构造(保强异常保证) 🐌 缓慢
└─ 否 ───────────▶ 只能移动(effects unspecified) ⚡ 别无选择决策在编译期由标准库(
vector实现)用类型萃取完成,无运行时开销;入口是std::move_if_noexcept。 注意:move 非noexcept时不是无条件拷贝——还要看是否可拷贝;不可拷贝的类型只能移动(标准允许,异常后果 unspecified)。
| 类型特征 | 扩容行为 | 性能 | 为什么 |
|---|---|---|---|
is_trivially_copyable | memmove 位块搬移 | 🚀 极快 | 纯内存复制,不调构造/析构 |
非 trivial,move 是 noexcept | 移动构造循环 | 🛵 良好 | 所有权转移(指针交换),抛不出、安全 |
| 非 trivial,move 可能抛 且可拷贝 | 拷贝构造循环 | 🐌 缓慢 | 源对象不敢动,保证扩容失败可回滚 |
| 非 trivial,move 可能抛 且不可拷贝 | 移动构造循环 | ⚡ 别无选择 | 无拷贝可用,只能移动 |
三、标准原文(出处)
1) std::move_if_noexcept 的定义 —— 见 C++ draft [utility]:
cpp
template<class T>
constexpr conditional_t<
!is_nothrow_move_constructible_v<T> && is_copy_constructible_v<T>,
const T&, T&&>
move_if_noexcept(T& x) noexcept;即:当「移动可能抛异常 且 类型可拷贝」时返回 const T&(走拷贝),否则返回 T&&(走移动)。
2) vector 单个元素插入在异常下的保证 —— 见 C++ draft [vector.modifiers](§2):
If an exception is thrown while inserting a single element at the end and
Tis Cpp17CopyInsertable oris_nothrow_move_constructible_v<T>is true, there are no effects. Otherwise, if an exception is thrown by the move constructor of a non-Cpp17CopyInsertableT, the effects are unspecified.
翻译与解读:
- 插入(含扩容搬迁)过程中抛异常时,只要
T可拷贝(能回滚)或 move 是noexcept(不会抛),就保证 "no effects"(强异常保证,如同什么都没发生); - 只有当
T既不可拷贝、move 又可能抛时,才出现unspecified(不可回滚),此时实现只能移动。
四、为什么要这么设计(核心在"异常安全")
扩容搬迁是个**"全有或全无"**的操作:新内存已分配、旧元素要逐个搬过去。若搬到一半抛异常:
- 用拷贝:源对象保持完好 → 可以回滚,保证 reallocation 的强异常保证(strong exception safety);
- 用移动:已被搬过的源对象处于"有效但未指定"(被掏空)状态 → 无法回滚,强保证被破坏。
所以标准层面对 vector 的取舍是:
- 能安全移动就移动——移动通常远比拷贝便宜(如
string、vector、unique_ptr),性能优先; - 移动可能抛 且 可拷贝 → 退化为拷贝——用一点点性能换取异常安全;
- 不可拷贝则只能移动——此时放弃强保证(标准写明 effects unspecified),因为别无选择。
⚠️ 实操坑:一个"移动很便宜、但忘了标
noexcept"的类型,在扩容时会被拷贝。给移动构造/移动赋值加noexcept(在确实不抛的前提下)是实打实的性能优化。
五、Trivial(平凡类型)扮演什么角色
- 平凡类型的移动必然是
noexcept:trivial move constructor 一定noexcept,所以它天然落在"移动"分支。 - 实现还会再进一步优化:对
is_trivially_copyable的类型,libstdc++/libc++ 在搬迁时走位块搬移(memmove/std::uninitialized_copy的 trivial 快路径),既不算拷贝构造也不算移动构造。 - 这是因为平凡类型拷贝与移动的位模式完全相同,"搬到新地址"就是"复制字节"。
- 相关概念 "trivially relocatable"(可平凡重定位)目前仍是非标准提案(P1144),但各大实现已在按
is_trivially_copyable做优化。
判断口诀:is_trivially_copyable_v<T> 为真 → 扩容时基本等价于 memmove,开销最小。
六、怎么判断:一段可运行的验证代码
cpp
#include <cstdio>
#include <type_traits>
#include <utility>
#include <vector>
struct Plain { int a[16]; }; // 平凡类型
struct CopyOnly { // 可拷贝、move 非 noexcept
int v;
CopyOnly(int x=0):v(x){}
CopyOnly(const CopyOnly& o):v(o.v){ std::puts("COPY ctor"); }
CopyOnly(CopyOnly&& o):v(o.v){ std::puts("MOVE ctor"); } // 未标 noexcept
};
struct Movable { // 可拷贝、move 是 noexcept
int v;
Movable(int x=0):v(x){}
Movable(const Movable& o):v(o.v){ std::puts("COPY ctor"); }
Movable(Movable&& o) noexcept :v(o.v){ std::puts("MOVE ctor"); }
};
struct NoCopy { // 不可拷贝、move 非 noexcept
int v;
NoCopy(int x=0):v(x){}
NoCopy(const NoCopy&) = delete;
NoCopy(NoCopy&& o):v(o.v){ std::puts("MOVE ctor"); }
};
int main() {
// 静态断言:直接看 move_if_noexcept 的推导结果类型
static_assert(std::is_same_v<
decltype(std::move_if_noexcept(std::declval<CopyOnly&>())), const CopyOnly&>);
static_assert(std::is_same_v<
decltype(std::move_if_noexcept(std::declval<Movable&>())), Movable&&>);
static_assert(std::is_same_v<
decltype(std::move_if_noexcept(std::declval<NoCopy&>())), NoCopy&&>);
std::vector<CopyOnly> a; a.reserve(1);
a.emplace_back(0); std::puts("-- grow CopyOnly --");
a.emplace_back(1); // 扩容:预期打印 COPY ctor
std::vector<Movable> b; b.reserve(1);
b.emplace_back(0); std::puts("-- grow Movable --");
b.emplace_back(1); // 扩容:预期打印 MOVE ctor
}预期输出(gcc/clang):
CopyOnly扩容 →COPY ctor(因为 move 非noexcept且有拷贝)Movable扩容 →MOVE ctorstd::vector<Plain>扩容 → 什么都不打印(trivial 位块搬移)
七、扩容策略与相关考点(顺带)
- 增长因子:libstdc++ 用 2×,MSVC 用 1.5×(
1.5更易复用此前释放的内存块,减少虚拟地址碎片;2× 增长更快、拷贝更少)。无论哪个,push_back均摊复杂度都是 O(1)。 - 搬迁代价:reallocation 的复杂度是线性(要搬 N 个元素),但平摊到每次
push_back是常数。 reserve/resize/shrink_to_fit:reserve一次到位避免多次扩容;shrink_to_fit是非强制请求。- 迭代器/引用失效:reallocation 会使整个容器的迭代器、指针、引用失效(含
end());不发生 reallocation 的插入只使插入点之后失效。 - 哪个容器"搬元素":只有
vector(及string的扩容)会在扩容时搬迁元素;deque/map/unordered_map扩容(rehash)时元素节点原地不动(只改指针/桶),不涉及该规则。
八、高频问答
Q1. vector 扩容时,元素到底是拷贝还是移动?怎么判断?
答案
由 std::move_if_noexcept 决定:move 构造是 noexcept,或类型不可拷贝 → 移动;可拷贝但 move 可能抛 → 拷贝。 即 !is_nothrow_move_constructible_v<T> && is_copy_constructible_v<T> 时走拷贝,否则走移动。
Q2. 为什么这么设计?和 noexcept / 异常处理有关吗?
答案
有关,核心是强异常保证。搬迁要"全有或全无":拷贝时源对象不变、可回滚;移动时源已被掏空、无法回滚。所以"移动可能抛且能拷贝"就退化为拷贝以保安全;"move 是 noexcept"则可放心移动;"不可拷贝"只能移动、放弃强保证(标准写 unspecified)。
Q3. Trivial 类型扮演什么角色?
答案
平凡类型的 move 必 noexcept,天然走移动;更进一步,trivially copyable 的类型由实现用 memmove/memcpy 位块搬移,连构造都不调用("trivially relocatable" 优化,标准尚未正式命名)。
Q4. 我明明写了移动构造函数,为什么扩容还是调用拷贝?
答案
大概率是移动构造没标 noexcept。只要它可能抛异常、且类型又可拷贝,move_if_noexcept 就返回 const T& 走拷贝。给它(在确实不抛时)加 noexcept 即可让扩容走移动。
Q5. std::move_if_noexcept 的定义是什么?
答案
cpp
template<class T>
constexpr conditional_t<
!is_nothrow_move_constructible_v<T> && is_copy_constructible_v<T>,
const T&, T&&>
move_if_noexcept(T& x) noexcept;"移动可能抛 且 可拷贝" → 返回 const T&(拷贝),否则返回 T&&(移动)。
Q6. deque / unordered_map 扩容会移动元素吗?
答案
不会(就这条规则而言)。deque 分段存储、扩容只是增加中控 map 的段指针;unordered_map rehash 只重排桶指针,节点原地不动。真正在扩容时"搬元素"的典型是 vector(和 string 的扩容)。
Q7. 扩容因子为什么常见 2 或 1.5?
答案
都要保证 push_back 均摊 O(1)。1.5×(MSVC)更易复用之前释放的内存块、减少地址空间碎片;2×(libstdc++)增长更快、搬迁次数更少。固定增量则是每次 O(N),退化成均摊 O(N)。
Q8. 怎么让自己的类型在扩容时走移动、且不破坏异常安全?
答案
在移动构造/移动赋值确实不抛的前提下标 noexcept(并遵守 Rule of Five/Zero)。这样既拿到移动的性能,又让 vector 的强异常保证生效。
九、八股速答(面试问法 → 一句话)
| 面试问法 | 一句话答案 |
|---|---|
| vector 扩容是拷贝还是移动? | 看 move_if_noexcept:move 是 noexcept 或不可拷贝 → 移动,否则拷贝 |
| 在哪一步决定? | 编译期由库用类型萃取决定,入口 std::move_if_noexcept |
| 为什么不是一律移动? | 移动可能抛异常时无法回滚,会破坏强异常保证 |
| 我写了移动构造为什么还拷贝? | 移动构造没标 noexcept(且类型可拷贝) |
| 平凡类型扩容做了什么? | memmove 位块搬移,不调构造/析构 |
| 怎么让扩容走移动? | 移动构造/移动赋值标 noexcept |
| 不可拷贝类型扩容呢? | 只能移动,异常后果 unspecified |
| 扩容的时间复杂度? | 单次线性,均摊到 push_back 是 O(1) |
string 也遵循吗? | 是的,扩容搬迁同理(SSO 时不涉及堆搬迁) |
is_trivially_copyable 的条件? | 拷贝/移动构造、赋值、析构全平凡,且无虚函数/虚基类 |
相关
vector底层实现、手写 vector、push_backvsemplace_back:vector.md- 迭代器失效大全:
iterator.md - 左值/右值、移动语义、完美转发:
../modern-cpp/move-semantics.md - RAII 与异常安全:
../modern-cpp/raii.md - 容器选型与复杂度:
container-selection.md
参考
- C++ 标准草案
[utility](move_if_noexcept声明)与[vector.modifiers](异常保证措辞):https://eel.is/c++draft/utility 、https://eel.is/c++draft/vector.modifiers - cppreference:
std::move_if_noexcept、std::vector(注:cppreference 对自动化访问有 Cloudflare 校验,本地直连可能被拦,可换浏览器访问)