std::vector扩容时仅当元素类型满足std::is_nothrow_move_constructible_v为true(即移动构造函数显式声明noexcept)才调用移动构造,否则退化为深拷贝;该行为由标准强制规定,非编译器优化选择。

不加 noexcept,std::vector 扩容时根本不会调用你的移动构造函数,而是默默退化为拷贝——你写的移动语义形同虚设。
std::vector 扩容时到底怎么选移动还是拷贝?
标准库不是靠“有没有移动构造函数”做判断,而是查 std::is_nothrow_move_constructible_v<t></t>。这个 trait 为 true 的前提,是移动构造函数显式声明了 noexcept(或 noexcept(true)),编译器才敢在重分配内存时放心移动元素。
- 若没写
noexcept,哪怕函数体里什么异常都没抛,trait 也是false,std::vector::push_back或reserve触发扩容时一律走拷贝路径 - 这个逻辑藏在
std::move_if_noexcept和容器内部的__relocate/__construct_at实现中,不是约定俗成,而是标准强制要求 - 你可以用
static_assert(std::is_nothrow_move_constructible_v<myclass>, "move must be noexcept");</myclass>在编译期卡住错误定义
哪些操作必须是 noexcept 才能安全移动?
移动构造函数本身要满足两个硬性条件:不抛异常 + 显式标注 noexcept。常见踩坑点包括:
- 成员变量的移动操作本身得是
noexcept:比如std::vector、std::string、std::unique_ptr的移动构造都是noexcept,但自定义类若没标,就会传染失效 - 构造函数初始化列表中不能调用可能抛异常的函数(如
new、std::string的某些构造);仅指针赋值、置空、整数拷贝这类操作才是安全的 - 函数体内不能有
throw、不能调用未标记noexcept的第三方函数、不能触发隐式异常(如访问空指针)
为什么编译器不自动推导 noexcept?
因为 C++ 标准规定:未显式声明 noexcept 的函数,默认视为“可能抛异常”。这不是保守,而是语言设计原则——异常规范必须由开发者明确承诺,不能靠静态分析猜测。
立即学习“C++免费学习笔记(深入)”;
- 即使你写了
MyClass(MyClass&&) : data_(other.data_) { other.data_ = nullptr; }这种明显安全的代码,不加noexcept就不算数 - 加了
noexcept后,若函数意外抛出异常,程序会直接调用std::terminate(),不栈展开——这是代价,也是约束力的体现 - 模板元编程中常用
noexcept(expression)做条件推导,比如noexcept(std::declval<t>().move())</t>,但手写移动构造函数请统一用无参数noexcept
真正容易被忽略的是:移动赋值运算符也得加 noexcept,否则 std::vector::assign、std::sort 等算法同样会回退到拷贝。一个类型只要有一个移动操作没标 noexcept,整个移动语义链就断了。


















