C++中noexcept关键字提出动机和运用

一、noexcept 是为了解决什么问题?

在 C++11 之前,异常说明使用的是动态异常规范

void f() throw(int, std::bad_alloc);
void g() throw();   // 表示不抛异常

问题极其严重:

  1. 运行期检查,零优化空间
  2. 违反即调用 unexpected(),再 terminate()
  3. ABI 不稳定,编译器难以优化
  4. STL 无法据此做容器级别决策

几乎没人敢用


C++11 的设计目标

noexcept 的核心动机是:

让“是否会抛异常”成为一个可在编译期推导、可用于优化、可影响接口选择的属性

换句话说:

异常是否发生,从「运行期契约」升级为「类型系统的一部分」


二、noexcept 的本质语义(非常重要)

void f() noexcept;

并不是说:

“这个函数不会抛异常”

而是说:

如果这个函数抛异常,程序将立刻调用 std::terminate()

即:

try {
f();
} catch (...) {
std::terminate();  // 无条件
}

noexcept承诺,不是能力检测。


三、noexcept 的两种形式

1.无条件 noexcept

void f() noexcept;

等价于:

void f() noexcept(true);

2.条件 noexcept(C++11 核心设计)

template<typename T>
  void foo(T&& x) noexcept(noexcept(T(std::forward<T>(x))));

异常规格成为编译期表达式

示例:完美转发构造

template<typename T>
  T make() noexcept(noexcept(T())) {
  return T();
  }

四、noexcept 是类型系统的一部分

void f() noexcept;
void g();
using F = void(*)();
using NF = void(*)() noexcept;
F  pf = g;   // OK
NF pnf = f;  // OK

但:

NF pnf = g;  //  编译错误

noexcept 是函数类型签名的一部分。


五、为什么 noexcept 对性能至关重要?

1.影响代码生成(EH tables)

  • 有异常 → 生成 异常展开表
  • noexcept完全移除异常元数据

在 hot loop / 数值计算 / SLAM 后端中尤为关键。


2.STL 的核心决策依据

std::vector 扩容行为

if (T is noexcept-move-constructible)
使用 move
else
使用 copy

等价于:

std::is_nothrow_move_constructible_v<T>

示例:为什么没写 noexcept 会导致性能灾难

struct Bad {
Bad(Bad&&) {}   //  没有 noexcept
};
struct Good {
Good(Good&&) noexcept {}
};
std::vector<Bad> v1;   // 扩容时 copy
  std::vector<Good> v2;  // 扩容时 move

这就是 STL 要求 move ctor noexcept 的原因


六、noexcept 与移动语义的关系(核心)

Rule of Five + noexcept

struct X {
X(X&&) noexcept = default;
X& operator=(X&&) noexcept = default;
};

原因

  • vector, deque, map 等容器
  • std::optional, std::variant
  • std::unique_ptr

全部依赖 noexcept 来选择移动路径


七、noexcept 与析构函数(极其重要)

C++11 起

~T() noexcept(true);  // 默认

即:

析构函数隐式 noexcept

如果析构函数抛异常?

~T() {
throw std::runtime_error("boom");
}

std::terminate()

原因:防止 stack unwinding 二次异常


正确模式

~T() noexcept {
try {
cleanup();
} catch (...) {
log_error();
}
}

八、noexcept 与模板元编程

常见 trait

std::is_nothrow_move_constructible<T>
  std::is_nothrow_copy_constructible<T>
    std::is_nothrow_destructible<T>

典型应用(SLAM / 点云库中很常见)

template<typename T>
  void safe_swap(T& a, T& b) noexcept(
  std::is_nothrow_move_constructible_v<T> &&
    std::is_nothrow_move_assignable_v<T>
      ) {
      T tmp = std::move(a);
      a = std::move(b);
      b = std::move(tmp);
      }

九、noexcept vs const

属性是否属于类型
const
noexcept
throw()否(已废弃)

十、常见误区(非常重要点)

误区 1:noexcept = 不会抛异常

事实
noexcept 的语义是 “一旦抛异常,立即 std::terminate()


错误理解示例

#include <iostream>
  #include <stdexcept>
    void f() noexcept {
    std::cout << "before throw\n";
    throw std::runtime_error("boom");
    std::cout << "after throw\n";
    }
    int main() {
    f();
    }

运行结果

before throw
terminate called after throwing an instance of 'std::runtime_error'
  • catch 根本来不及
  • 栈不会正常展开
  • 析构函数不会全部执行

对比:非 noexcept

void g() {
throw std::runtime_error("boom");
}
int main() {
try {
g();
} catch (const std::exception& e) {
std::cout << "caught: " << e.what() << '\n';
}
}

输出

caught: boom

正常异常语义


工程结论

noexcept 是“强终止契约”,不是“不会抛”的保证


误区 2:随便给函数加 noexcept

这是生产事故级错误


错误示例:包装函数

void may_throw() {
throw std::runtime_error("error");
}
void wrapper() noexcept {
may_throw();   // 
}
int main() {
wrapper();
}

运行结果

terminate called after throwing an instance of 'std::runtime_error'

更隐蔽的版本(真实工程坑)

void log(const std::string& s) {
if (s.empty()) {
throw std::logic_error("empty");
}
}
void foo() noexcept {
log("");   // 间接抛异常
}

根本看不到 throw,却直接 terminate


正确写法 1:内部吞异常

void foo() noexcept {
try {
log("");
} catch (...) {
// fallback / logging
}
}

正确写法 2:条件 noexcept

template<typename F>
  void call(F&& f) noexcept(noexcept(f())) {
  f();
  }

工程结论

只有当“整个调用链都不抛异常”时,才可以写 noexcept


误区 3:忘记给 move ctor 加 noexcept

这是 STL 性能退化最常见的来源


错误示例

#include <vector>
  struct Bad {
  Bad() = default;
  Bad(const Bad&) = default;
  Bad(Bad&&) {}   //  没有 noexcept
  };
  int main() {
  std::vector<Bad> v;
    v.reserve(1);
    v.emplace_back();
    v.emplace_back();  // 触发扩容
    }

STL 的真实逻辑

if (is_nothrow_move_constructible<T>)
  move
  else
  copy

结果

  • 扩容时 调用 copy ctor
  • 大对象 → 灾难性性能
  • 对 Eigen / 点云 / 位姿对象尤其致命

正确示例

struct Good {
Good() = default;
Good(const Good&) = default;
Good(Good&&) noexcept {}  // 
};

对比验证(可加日志)

struct Verbose {
Verbose() = default;
Verbose(const Verbose&) {
std::cout << "copy\n";
}
Verbose(Verbose&&) noexcept {
std::cout << "move\n";
}
};
std::vector<Verbose> v;
  v.emplace_back();
  v.emplace_back();

输出

move

如果去掉 noexcept,输出是:

copy

工程级总结

一个没写 noexcept 的 move ctor,等价于“禁用移动语义”


三个误区一句话总结

误区本质错误
noexcept = 不会抛实际是“抛了就死”
随便加 noexcept违反调用链异常安全
move ctor 没 noexceptSTL 主动退化到 copy

十一、工程级使用准则

必须 noexcept

场景
移动构造 / 移动赋值
析构函数
swap
RAII cleanup
数值内核、实时系统

谨慎使用

场景
构造函数(分配内存)
IO
用户回调

不要使用

场景
无法保证内部调用链不抛异常

十二、一个完整工程示例

struct Pose {
Eigen::Matrix4d T;
Pose() = default;
Pose(Pose&& other) noexcept
: T(std::move(other.T)) {}
Pose& operator=(Pose&& other) noexcept {
T = std::move(other.T);
return *this;
}
~Pose() noexcept = default;
};

这类类型在 SLAM 后端、图优化、点云容器 中是黄金标准


十三、总结一句话

noexcept 不是语法糖,而是现代 C++ 性能、异常安全和库设计的核心支点

posted @ 2026-01-29 13:41  yangykaifa  阅读(2)  评论(0)    收藏  举报