第20.9章:遗留异常在这里根本不存在

English

1. 这个特性

P1105R0 第 4.1 节是启动现代独立实现工作的那一节,而它的第一句就是论证:内核和嵌入式环境并非普遍负担得起异常,而抛出异常"在 Itanium ABI 上需要一次堆分配,在 Microsoft ABI 上需要一大块栈分配",两者在内核里都不合适。

19.3 讲了机制,19.8 实测了它。本页只问独立实现的问题:它向环境要求什么,而当那个环境本身就是内核时你该怎么办?

2. 实测:十一个符号

#include <stdexcept>

void fail(int x) noexcept(false) { if (x < 0) { throw ::std::runtime_error("negative"); } }

int main() { try { fail(-1); } catch (::std::runtime_error const &) { return 0; } return 1; }
$ g++ -std=c++26 -O2 -c legacy.cc && nm -u legacy.o
                 U _Unwind_Resume
                 U _ZNSt12__cow_stringC2EPKc
                 U _ZNSt13runtime_errorD1Ev
                 U _ZTISt13runtime_error
                 U _ZTVSt13runtime_error
                 U __cxa_allocate_exception
                 U __cxa_begin_catch
                 U __cxa_end_catch
                 U __cxa_free_exception
                 U __cxa_throw
                 U __gxx_personality_v0

十一个,而每一个都是一项操作系统设施的组成部分:

这里面没有任何子集是你自己提供得了的。写它们意味着写一个运行时库,为你的目标编译,与编译器的期望 ABI 兼容,然后在每个编译器版本上维护它。这就是 20.3 所说的、让 C++03 的独立模式成为谎言的那笔负担。

3. 在内核里没有任何人可以问

在宿主系统上,unwinder 来自 libgcc_s.so,帧表由 ld.so 注册,堆是 brk/mmap。内核就是这些东西本该建在其上的那一层。如果它想要遗留异常,它必须实现一个跨越自己中断帧的 unwinder、一个针对自己可加载模块的异常表注册表,以及一个可重入到足以从出错上下文中被调用的堆 —— 然后它就是在内核里维护一份私有的 C++ 运行时,这就是问题本身的定义。

还有第二个理由,与机械装置毫无关系:在内核上下文里抛出的异常会在某一帧恢复。哪一帧?一个已经被杀掉的进程?一个寄存器状态已被破坏的中断处理程序?遗留异常假定被展开到的那一帧是一条健康线程里的函数调用,而在内核里这个假定根本不成立。这就是"对内核尤其坏"的含义(20.4)。

4. 在 GPU 上根本没有它的任何版本

内核至少可能以极大的代价构建出那个运行时。设备代码做不到:每一种 GPU 编程模型都要求关闭异常,设备运行时里没有 unwinder,没有动态加载器,没有异常表,也没有地方存放被抛出的对象 —— 一个抛出异常的线程无处可去,因为同一个 warp 和 block 里的其他线程并不在它的调用栈里。

环境遗留异常需要什么那里有什么
独立 / 裸机 Unwinder、帧表、堆、RTTI、加载器 除非你带过去,什么都没有
内核 上述全部,外加一个可定义的恢复帧 内核自身,而它不能假定其中任何一项
GPU 设备代码 上述全部 根本不在这个模型里;异常按规范要求是关闭的
WebAssembly 把展开栈作为目标特性 基础目标里没有;提案把它加到引擎里,而不是加到你的二进制里
任何 -fno-exceptions 构建 —— error: exception handling disabled, use '-fexceptions' to enable

五行,其中四行的答案是"这个机制不可用"。一门其唯一错误通道是这个机制的语言,就是一门在那四个目标上都无法报告错误语言 —— 而变通办法是手工把错误码穿过每个函数,那正是 C++ 用户被告知已经逃离的东西。

5. 所以它们是被禁用的,而不是被劝阻的

这就是为什么 19.7 不说"优先用赫布异常",而是说把 -fno-exceptions 加进构建,让一个遗留 throw 成为编译错误。在独立目标上,一个没有禁用它们的构建,是一个能在桌面上链接、在板子上失败的构建 —— 那是两个世界中最糟的。

下一页是另一半:那个在这里确实能用的机制,只要一个链接符号而不是十一个。