第19.7章:用构建选项禁止遗留异常

English

1. 选项清单

在文档里写"我们不使用遗留异常"什么也不会改变:半年后有人会引入一个会抛出的头文件,unwinder 又回到了链接里,而在某个无法展开栈的目标上构建失败之前,没人会注意到。让它变成机械性的。你编译的每一个翻译单元,始终加上:


-fno-exceptions -fno-rtti -fno-unwind-tables -fno-asynchronous-unwind-tables -fno-ident

每一个各删掉了什么:

选项 它让什么变得不可能
-fno-exceptions throw、try、catch 在构建里任何位置都无法使用,随之消失的还有整套 __cxa_* 运行时和 personality routine。赫布异常照常工作:throws 和 catch throws 是另一套机制,不受影响
-fno-rtti typeid、dynamic_cast,以及遗留异常处理块匹配所依赖的那些类型信息对象。赫布异常不使用 RTTI,所以 throws 通道上没有任何东西受影响
-fno-unwind-tables 栈遍历器所需的 .eh_frame 帧表
-fno-asynchronous-unwind-tables 同样这些表的异步形式,调试器和性能分析器也会用到它们
-fno-ident 编译器写在 .comment 里的标识字符串,它们泄露构建机器的细节,在二进制里纯粹是噪音

这些不是风格问题。每一条都删掉一个机制,而一旦有人试图使用它,构建就会大声失败。

2. 它究竟删掉了什么

一个小程序,一个赫布异常,编译两次:一次用默认选项,一次用上面那份清单。


#include <fast_io.h>

int divide(int a, int b) throws
{
	if (b == 0)
	{
		throw throws ::std::errc::invalid_argument;
	}
	return a / b;
}

int main()
{
	int result{};
	try
	{
		result = divide(84, 2);
	}
	catch throws (::std::error e)
	{
		::fast_io::io::print("error ", e, "\n");
		return 1;
	}
	::fast_io::io::print("84/2 = ", result, "\n");
}


84/2 = 42

输出相同,二进制更小,而差值恰好就是程序从未使用的那套机制的机器码:

默认选项 禁令清单 它是什么
5,869 bytes of .text 4,859 bytes 代码,包括 personality routine 能触达的部分
.eh_frame 644 bytes 128 bytes unwinder 遍历的那些帧表
.eh_frame_hdr 116 bytes 44 bytes 对那些帧表做二分查找的索引
.gcc_except_table 16 bytes 消失 语言异常表:区间、landing pad、处理块类型
13 FDEs 4 FDEs 到底有多少函数带有展开信息
.comment 111 bytes 69 bytes 编译器版本字符串

剩下的四个 FDE 来自 C 运行时的启动对象,而不是你的代码,所以这已经是最低值了。在没有加载器的目标上,没有任何东西可供注册它们,而这正是重点:这些选项让你的程序所提出的要求,与它要运行的那些平台实际具备的东西对齐。

3. 你想要的那条诊断

有了禁令清单,遗留异常会在引入它的那一行变成编译错误,而不是在某个无法展开栈的目标上变成一次莫名其妙的运行时失败:


legacy_demo.cc: In function 'int main()':
legacy_demo.cc:13:78: error: exception handling disabled, use '-fexceptions' to enable
   13 |                                 throw ::std::runtime_error("division by zero");
      |                                                                              ^

4. 放进构建,而不是放进维基

用普通的 makefile:


CXXFLAGS += -std=c++26 -fherbceptions \
            -fno-exceptions -fno-rtti -fno-unwind-tables \
            -fno-asynchronous-unwind-tables -fno-ident
LDFLAGS  += -lherbceptions

用 CMake,并且应用到项目里的每一个目标,而不只是可执行文件:


add_compile_options(
  -fherbceptions
  -fno-exceptions -fno-rtti
  -fno-unwind-tables -fno-asynchronous-unwind-tables
  -fno-ident)

add_compile_options 故意是全局的:一个没有加这些选项编译出来的静态库,能把那套行为重新带进用这些选项编译出来的可执行文件,而失败随后会在一个看起来毫不相关的目标上、在链接时冒出来。一份清单,所有目标,包括测试和基准。

5. 你放弃了什么,以及为什么那不是损失

如果你只从本章拿走一行,就拿这份选项清单。用它的项目无法再漂回遗留异常,而每一个没有 unwinder、没有加载器、没有 C++ 运行时的目标,都仍然是你能够构建的目标。