第20.9章:遗留异常在这里根本不存在
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
十一个,而每一个都是一项操作系统设施的组成部分:
-
__cxa_allocate_exception—— 一个堆。在没有堆的目标上,这个函数写不出来。 -
_Unwind_Resume—— 一个 unwinder,一个理解目标栈帧布局的栈遍历器。 -
__gxx_personality_v0—— 每一帧的回调,决定这一帧是否捕获这个异常。它读取沿途每个函数的语言异常表.gcc_except_table。 -
_ZTISt13runtime_error、_ZTVSt13runtime_error—— RTTI 和 vtable,用于把处理块与抛出的类型匹配(20.8)。 -
帧表本身,
.eh_frame,而且必须向一个加载器注册,好让 unwinder 找到它们。
这里面没有任何子集是你自己提供得了的。写它们意味着写一个运行时库,为你的目标编译,与编译器的期望 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 成为编译错误。在独立目标上,一个没有禁用它们的构建,是一个能在桌面上链接、在板子上失败的构建 —— 那是两个世界中最糟的。
下一页是另一半:那个在这里确实能用的机制,只要一个链接符号而不是十一个。