c++的异常安全(exception safety)是什么级别? (nothrow保证)

nothrow保证即noexcept保证,指函数绝不会抛出任何异常,违反时程序调用std::terminate()终止;C++11起统一用noexcept说明符,其对move操作和容器优化至关重要。

什么是 nothrow 保证(noexcept guarantee)

在 C++ 中,nothrow 保证(更准确应称 noexcept guarantee)是异常安全的最高级别:函数承诺**绝不会抛出任何异常**。它不是“可能不抛”,而是编译器可验证、运行时强制的契约——若违反(比如 noexcept 函数内部抛了异常),程序会立即调用 std::terminate() 终止。

注意:nothrow 是旧式写法(C++98/03 中用于 new 表达式),C++11 起统一用 noexcept 说明符或说明符表达式。现在说 “nothrow 保证” 实际指 noexcept 语义下的强保证。

如何声明和验证 noexcept 函数

声明方式直接、显式,但容易误用:

  • void f() noexcept; —— 明确承诺不抛异常
  • void g() noexcept(true); —— 等价于上一行
  • void h() noexcept(false); —— 明确允许抛异常(默认行为)
  • template void swap(T& a, T& b) noexcept(noexcept(a.swap(b))); —— 使用 noexcept 操作符做条件推导

关键点:

  • 编译器不会自动推导 noexcept,即使函数体为空或只调用其他 noexcept 函数,也必须显式标注
  • noexcept 是函数类型的一部分:void(*)() noexceptvoid(*)() 是不同类型,不能互相赋值
  • 析构函数默认是 noexcept 的;若手动声明为 noexcept(false),且实际抛异常,仍会触发 std::terminate()

为什么 noexcept 保证对 move 操作和容器至关重要

标准库容器(如 std::vector)在扩容、重排时是否启用移动而非拷贝,取决于元素类型的移动操作是否具备 noexcept 保证:

  • std::vector::push_back 在需要重新分配时,若 T 的移动构造函数是 noexcept,则优先移动;否则退回到拷贝(因为拷贝失败可回滚,移动若抛异常则无法保证强异常安全)
  • std::vector::resizestd::sort 等算法也会依赖 noexcept 移动来启用优化路径
  • 自定义类型若提供移动操作,务必评估其是否真能不抛异常;若不能,就别标 noexcept,否则破坏容器行为
struct Widget {
    Widget(Widget&&) noexcept; // ✅ 若底层资源转移无异常风险(如 raw pointer 交换)
    // Widget(Widget&&) { /* 可能抛 std::bad_alloc */ } // ❌ 不该标 noexcept
};

常见陷阱与误判场景

看似安全的操作,实际可能隐式抛异常:

  • delete p; 本身不抛,但若 punique_ptr 且其自定义删除器抛异常,则整个移动/销毁可能破环 noexcept
  • std::vector::data()noexcept,但 std::vector::at() 不是(下标越界抛 std::out_of_range
  • 调用第三方库函数前,必须查文档或源码确认其是否真正 noexcept;仅看函数签名不够(比如未标注的函数默认 noexcept(false)
  • 模板函数中使用 noexcept 表达式时,若未覆盖所有实例化分支,可能在某些类型下意外变成 noexcept(false)

最易被忽略的是:异常安全级别不是静态属性,它依赖整个调用链的每一步——哪怕一个 noexcept 函数里调用了未标记的普通函数,整条路径就失去 noexcept 保证。