第20.3章:C++03 独立实现的谎言
1. "独立"在语言里,而不在库里
上一页说 C++03 要求了它实现不了的头文件。这一页讲同一个问题更糟的那一半:语言。在 C++03 里,宣布你的实现是独立实现只改变了头文件清单,别的什么都没变。语法完全一样。throw、try、catch、typeid、dynamic_cast、new 和 delete 全都还在,拼写完全一样,在任何函数里都能用。
如果你的目标没有堆也没有 unwinder,那就不是"特性更少",而是一门你按所写根本无法使用的语言。它的三个核心构造会编译成对目标上不存在的运行时的调用,而编译器会乐意替你生成它们,然后让链接器去抱怨。这是一个 throw 在一个裸目标上于链接期要付的代价:
void fail(int x) noexcept(false) { if (x < 0) { throw "negative"; } }
int main() { try { fail(-1); } catch (char const *) { return 0; } return 1; }
$ g++ -std=c++26 -O2 -nostdlib l2.cc -o l2
undefined reference to `_Unwind_Resume
undefined reference to `__cxa_allocate_exception
undefined reference to `__cxa_begin_catch
undefined reference to `__cxa_end_catch
undefined reference to `__cxa_throw
undefined reference to `__gxx_personality_v0
undefined reference to `typeinfo for char const*
那是一个只为一条比较语句的程序,需要的六个运行时入口点和一个类型信息对象。它们没有一个是你写得出来的,而这正是本页的要点。__cxa_allocate_exception 需要一个堆。_Unwind_Resume 就是 unwinder,它必须理解你目标的栈帧布局。__gxx_personality_v0 必须能匹配处理块,那需要 RTTI 层级,那需要 vtable 布局,那就是 ABI。"实现遗留异常"意味着你自己写一个 libsupc++,而你要写出来的东西,是一件披着库的外衣的操作系统设施。
2. 被迫的选择,以及为什么它其实不是选择
所以一次独立的 C++03 构建只有一条路:把特性关掉,然后不再符合标准。
| 语言要求的特性 | 它在运行期需要什么 | 独立构建当时怎么做 |
|---|---|---|
throw / catch |
异常对象的堆、unwinder、帧表、personality routine、RTTI | -fno-exceptions |
typeid, dynamic_cast |
type_info 对象、每个类的 RTTI 表 |
-fno-rtti |
new / delete |
一个全局堆、::operator new,以及一套失败策略 |
没有默认堆,或者手写一个 operator new |
thread_local |
一个线程指针和 TLS 映像的建立 | -fno-threadsafe-statics,或者干脆什么都没有 |
| Global constructors | 启动代码里的一段 .init_array 遍历 |
你自己的 crt0,或者手工静态初始化 |
每一行都意味着"标准说这能用;你的构建说它不能"。一门其独立模式要求你禁用自己核心特性的语言,不是一门有独立模式的语言。这就是 C++03 独立实现是一场谎言的含义 —— 不是委员会撒谎,而是那份文档描述了一样没有厂商能提供、没有用户能依赖的东西。
3. 最小的例子:std::addressof 当时并不存在
这里有一个语言自身设计留下、而库无法填补的窟窿 —— 而它只是一个函数。在 C++03 里,&x 是可重载的:任何类都可以定义 operator&,并改变"取地址"的含义。当时没有任何办法去要真正的地址。
::std::addressof 是后来才有的,而且它无法手写:它需要编译器魔法(__builtin_addressof)来绕过用户定义的 operator&,并且它必须是 constexpr。于是有很多年,语言的一个特性 —— 取对象的地址 —— 可以被悄悄重新定义,而整个库里没有任何逃生通道。C 自从 1972 年起,&x 就是 x 的地址。
完整的故事,包括现代那个用户定义的 operator& 仍然能让 &s == nullptr 为真的版本,在 17.8。它之所以属于本章,理由不同:::std::addressof 在 C++23 之前并不是独立实现必须提供的。所以一门用 C++ 写的内核可以有一个带敌意 operator& 的类、没有任何办法打败它,而标准并不承诺那条逃生通道在那里存在。一个不是独立实现的标准化特性就是不存在 —— 而这一个在最要紧的地方不存在。
4. 三十年"用编译选项凑合"
结果就是 1998 至 2023 年间实用意义上的独立 C++ 是一个由选项定义的方言,而不是由标准定义的:-fno-exceptions -fno-rtti -nostdlib -fno-threadsafe-statics -nostdinc++,加上一个手写的 operator new(如果你还想要一个),加上链接脚本,加上启动代码。每个嵌入式代码库都各自发明了这份清单,而按标准的字面,它们每一个都是不符合标准的 C++ 程序。
赫布异常改变了这件事,而这就是它们是本书铰链的原因:20.10 展示了同一个 throw/catch 程序只要求一个链接符号、一个数据指针,不需要 unwinder 也不需要堆。让 C++ 无法用于独立实现的特性并不是"错误处理";而是语言唯一的错误处理是架在操作系统之上实现的。