跳转至

noexcept 异常说明

标准 难度 状态 分级 证据 审阅 最后校验
C++11 3/5 final A 3 条(manual 3) 单审(zyuhao7) 2026-11-04

编译器支持:GCC 4.6+ · Clang 3.0+ · MSVC 19.0+。版本查询:cppreference 编译器支持。

主张与证据

主张 支持证据
查询接口与 swap 标注 noexcept,声明不抛异常 ev-drogon-noexcept-result-accessors-001
string_view 的访问器全部标注 noexcept ev-fmt-noexcept-string-view-002
移动构造与移动赋值声明 noexcept ev-abseil-noexcept-move-003

本站证据页:ev-abseil-noexcept-move-003 · ev-drogon-noexcept-result-accessors-001 · ev-fmt-noexcept-string-view-002

解决什么问题

C++03 的动态异常说明 throw(A, B) 在运行期检查,违反时调用 std::unexpected,而且编译器难以据此优化,标准库实现也普遍避免使用。调用方想知道一个函数是否可能抛异常,却没有可靠的编译期手段。

noexcept 把“不抛异常”变成函数类型的一部分:编译器可以在编译期查询,标准容器可以据此选择移动而非复制,异常安全边界也更清晰。标准提案为 N3050,语法属于核心语言。

基础用法

在函数声明或定义末尾写 noexcept,表示函数不抛出异常;noexcept(expr) 中 expr 为假时等价于可能抛出。noexcept(f(args)) 是运算符,在编译期返回布尔常量。

#include <iostream>
#include <type_traits>
#include <utility>
#include <vector>

class Buffer {
 public:
  Buffer() = default;
  Buffer(const Buffer&) = default;
  Buffer& operator=(const Buffer&) = default;
  Buffer(Buffer&&) noexcept = default;
  Buffer& operator=(Buffer&&) noexcept = default;
};

int may_throw(int value) { return value; }

int main() {
  std::cout << std::is_nothrow_move_constructible<Buffer>::value << ' '
            << noexcept(std::declval<Buffer>()) << '\n';
  std::cout << noexcept(may_throw(1)) << '\n';

  std::vector<Buffer> buffers;
  buffers.reserve(2);
  buffers.emplace_back();
  buffers.emplace_back();
  std::cout << buffers.size() << ' ' << buffers.capacity() << '\n';
}

输出 1 1、0 与 2 2。Buffer 的移动构造和移动赋值都标注 noexcept,因此 is_nothrow_move_constructible 为真;may_throw 没有异常说明,noexcept(may_throw(1)) 为假。完整版本见 examples/cpp11/noexcept.cpp。

noexcept 是承诺而不是检查:函数体内真的抛出异常时,程序会调用 std::terminate,不会退回到调用方。标注前要确认函数及其调用的所有代码都不会抛出。

进阶用法

移动构造与移动赋值通常应标注 noexcept。std::vector 扩容时,如果元素的移动构造是 noexcept,它会选择移动;否则为避免移动过程中抛异常导致容器处于不一致状态,它可能退回复制。对只管理指针或句柄的类型,这个标注直接影响性能。

析构函数默认隐式 noexcept,除非显式写 noexcept(false)。析构中抛出异常会在栈展开时触发 std::terminate,因此析构函数应吞掉或记录异常,而不是向外抛。

noexcept 可以用表达式条件化:void swap(T& a, T& b) noexcept(noexcept(a.swap(b))); 表示仅当底层 swap 不抛时才承诺不抛。标准库的容器与 std::pair 大量使用这种写法。

noexcept 运算符与异常说明的语法相同但语义不同:noexcept(expr) 在编译期求值,返回 bool;函数声明后的 noexcept 是接口契约。两者可以组合使用,让泛型代码自动传播异常规格。

C++17 起 noexcept 成为函数类型的一部分,函数指针赋值时异常规格必须兼容;这也让模板推导和重载能区分“可能抛”与“不抛”的版本。

真实项目证据

项目 位置 用法 为什么典型
drogon orm_lib/src/Result.cc#L121-L134 size、swap、columns 标注 noexcept 只转发容器操作,不会失败,标注后接口契约清晰
fmt include/fmt/base.h#L547-L555 string_view 访问器全部 noexcept 只读视图不可能抛异常,是视图类型的基本约定
abseil absl/status/status.h#L496-L500 移动构造与移动赋值标注 noexcept 只转移内部表示,让标准容器在扩容时优先选择移动

三条证据均为 manual 级:作者复核过出处与行号,尚未引入第二名审核者。三处都在生产路径,覆盖查询接口、视图访问器与移动操作三种典型场景。

陷阱与边界

noexcept 是终止承诺。 函数内抛出异常会直接 std::terminate;不要为了性能给可能失败的函数加 noexcept,分配、I/O、解析都可能抛异常。

不是编译期证明。 编译器不会检查函数体是否真的可能抛异常;noexcept 是调用方与实现者之间的契约,标错不会报错,只会在运行期终止。

虚函数要一致。 基类虚函数标注 noexcept 后,派生类覆盖也必须是 noexcept,否则编译失败;反过来基类可能抛、派生类不抛是允许的。

移动操作标错会掉性能。 移动构造不标 noexcept 时,std::vector 等容器在扩容时可能退回复制;对管理资源的类型,这是常见的性能陷阱。

构造函数抛异常不会终止,除非标注 noexcept。 普通构造函数可以抛;只有显式 noexcept 才会把异常变成终止。

析构函数默认 noexcept。 在析构中抛异常几乎必然终止程序;需要处理失败时用日志或错误码,不要向外抛。

与 throw() 不完全等价。 C++11 的 throw() 已弃用,语义接近 noexcept 但调用 std::unexpected;新代码统一用 noexcept。

库联动

  • <type_traits>:is_nothrow_move_constructible、is_nothrow_swappable 等特征让泛型代码查询异常规格。
  • 标准容器:std::vector 扩容、std::move_if_noexcept 都依赖元素的 noexcept 移动构造。
  • std::swap:标准实现按条件 noexcept,自定义类型的 swap 应与之一致。
  • fmt / abseil:视图访问器与移动操作在最底层标注 noexcept,上层接口可以安全地传播该保证。
  • drogon:查询接口只读内部指针,标注 noexcept 后调用方无需为它们写异常处理。

横向对比

方案 检查时机 违反后果 可被编译器利用 主要代价
noexcept 编译期 调用 terminate 是 标错会直接终止程序
动态异常说明 运行期 调用 unexpected 弱 C++11 起弃用,C++17 移除
不写异常说明 无 正常传播 否 调用方无法判断,移动优化可能失效
运行时 try/catch 运行期 局部处理 否 代码噪音大,无法表达接口契约

跨标准演进:C++11 通过 N3050 用 noexcept 取代动态异常说明并弃用 throw();C++17 将异常规格纳入函数类型并移除动态异常说明;C++20 没有改变核心语义,只在标准库中继续扩大 noexcept 的覆盖范围。