第3.16章: 赫布异常(Herbceptions)

English

概述

在之前的每一页中,我们都默默地假定没有任何操作会失败。从这里开始,这种沉默结束了: fast_io 中的每一个错误都是赫布异常(herbception),而 赫布异常是本教程中唯一的错误报告机制。

译名说明:herbception 是 Herb Sutter 提出的错误处理机制, 本教程译作赫布异常。代码标识符 (如 struct raw_herbception)、编译器选项 (-fherbceptions、-lherbceptions)以及编译器输出的 英文诊断信息,一律保留英文原名,不做翻译。

你只需要学两条语句:

读懂这一页不需要任何别的知识:下面每个例子都是一个单独的 int main(), 它抛出一个错误,然后再把它捕获。

1. 什么是赫布异常

赫布异常是一个双字的错误值:


struct raw_herbception
{
	::std::error_domain_singleton const *domain; // which error code-space
	::std::size_t code;                          // the code inside that space
};

无论是什么错误,sizeof(::std::error) == 2 * sizeof(void *)。 它通过返回寄存器送回给调用者,同时配合条件寄存器中的 一个失败位 —— x86 上是 CF,AArch64 上是 NZCV.C。没有第二条返回路径,也不需要任何设置:

赫布异常在两条路径上都非常非常廉价。用这种方式报告错误的函数, 生成的代码与返回错误码的函数相同,而失败路径只是返回两个字的普通返回。 永远不要为了“速度”而选择“返回错误码再检查”的风格, 也永远不要因为“抛异常听起来很贵”而不去报告失败。

2. 构建:-fherbceptions -lherbceptions

赫布异常是一项编译器特性,而不是库的小把戏,所以你需要一个实现了它的编译器。 只有我们的 GCC 与 LLVM 构建可以:

两个选项,这就是全部额外的添加:


# our Clang
clang++ --config=$HOME/cfgs/x86_64-linux-gnu-libcxx.cfg \
  -O3 -flto=thin -fherbceptions -I$HOME/libraries/fast_io/include \
  hello.cc -lherbceptions -o hello

# our GCC
g++ -std=c++26 -O3 -s -fuse-ld=lld -fherbceptions \
  -I$HOME/libraries/fast_io/include \
  hello.cc -lherbceptions -o hello

在 Windows 上,同样的选项配合 Windows 目标即可工作,而 --sysroot 能找到运行时,所以不需要写 -L:


clang++ --config=$HOME/cfgs/x86_64-windows-msvc.cfg \
  -O3 -flto=thin -fherbceptions -I$HOME/libraries/fast_io/include \
  hello.cc -lherbceptions -o hello.exe

x86_64-w64-mingw32-g++ -std=c++26 -O3 -s -fherbceptions \
  -I$HOME/libraries/fast_io/include \
  hello.cc -lherbceptions -lntdll -o hello.exe

忘记 -fherbceptions,编译器会拒绝这种语法;忘记 -lherbceptions,链接会因错误域符号失败。两者都在 第1.8章中讲过。

3. throw throws 语句

报告失败是一条语句,它只有一种形态:


throw throws expression;

throws 是标记,表示“这个错误从赫布异常通道离开”。 该表达式是某个带有错误域的类型的值 —— 实践中就是第 7 节列出的 标准错误枚举之一。


#include <fast_io.h>

using namespace ::fast_io::io;

int main()
{
	try
	{
		throw throws ::std::errc::invalid_argument;
	}
	catch throws (::std::error e)
	{
		perrln(e);
		return 1;
	}
	return 0;
}

输出,以及程序返回的退出状态:


[posix](22)Invalid argument
exit status: 1

三条规则,全部由编译器强制执行:

先检查,再报告。在代码块最前面检查条件, 并在任何东西被写出或打印之前立刻 throw throws。 fast_io 自身就是这么写的。

4. try { } catch throws (::std::error e) { } 语句

处理也是一条语句,只有一种形态:


try
{
	/* anything that may fail */
}
catch throws (::std::error e)
{
	/* inspect / report / recover / rethrow */
}

规则同样由编译器强制执行:

5. ::std::error:你会用到的成员

处理程序里的值非常小:两个字,以及六件你可以问它的事。 这就是你需要知道的完整清单。

表达式 结果 含义
e.domain() ::std::error_domain_singleton const * 该错误属于哪个码空间。在处理程序内部它绝不为空。
e.code() ::std::size_t 该域内的原始编码 —— 对 POSIX 错误而言就是 errno 值。
e == ::std::errc::permission_denied bool 精确匹配:同一个域并且同一个编码。右侧值的类型 决定了比较所用的域。
e.equivalent(::std::errc::no_such_file_or_directory) bool 跨域匹配:这个错误与那个编码是否含义相同? HTTP 的 404 就以这种方式等于 ENOENT。
e.to_errc() ::std::errc 该错误的 POSIX 等价物,以 ::std::errc 表示。
e.is_code_of<::std::http_errc>() bool “这个错误是由这个域产生的吗?”,忽略具体编码。

#include <fast_io.h>

using namespace ::fast_io::io;

int main()
{
	try
	{
		throw throws ::std::http_errc::not_found;
	}
	catch throws (::std::error e)
	{
		println("domain is http: ", e.is_code_of<::std::http_errc>());
		println("domain is posix: ", e.is_code_of<::std::errc>());
		println("== not_found: ", e == ::std::http_errc::not_found);
		println("== errc ENOENT: ", e == ::std::errc::no_such_file_or_directory);
		println("equivalent: ", e.equivalent(::std::errc::no_such_file_or_directory));
		println("to_errc: ", static_cast<::std::int_least32_t>(e.to_errc()));
		println("code: ", e.code());
	}
}

domain is http: 1
domain is posix: 0
== not_found: 1
== errc ENOENT: 0
equivalent: 1
to_errc: 2
code: 404

把 == 与 equivalent 放在一起看。该错误是 http_errc::not_found,所以它不是一个 ENOENT (域不同:== 为假),但它含义相同 (所以 equivalent 为真)。这个差别正是这两个成员都存在的原因。

6. 打印错误

因为错误只是一个域加一个编码,fast_io 可以打印它。完整形式是 [domain](code)message,你也可以只要求 其中的一部分:


#include <fast_io.h>

using namespace ::fast_io::io;

int main()
{
	try
	{
		throw throws ::std::errc::no_space_on_device;
	}
	catch throws (::std::error e)
	{
		perrln(e);                                          // the whole line, to stderr
		perr("inside a line: ", e, "\n");
		perr("domain name: ", ::fast_io::mnp::name(e), "\n");
		perr("code + text: ", ::fast_io::mnp::message(e), "\n");
		perr("as posix name: ", ::fast_io::mnp::posix_equivalent_name(e), "\n");
		perr("as posix text: ", ::fast_io::mnp::posix_equivalent_message(e), "\n");
	}
}

[posix](28)No space left on device
inside a line: [posix](28)No space left on device
domain name: posix
code + text: (28)No space left on device
as posix name: posix
as posix text: [posix](28)No space left on device
perr 写到 stderr,print 写到 stdout。 本教程中错误报告都写到 stderr,这样即使程序的输出被重定向到文件, 失败信息仍然会显示在终端上。报告错误只是一次普通的 I/O 调用:不需要在外面包任何特别的东西。

7. 错误域

域就是一个码空间。每一个域都是一种类型,带有错误域特化, 并各有自己打包 code() 的方式。它们全都在这里了。 你永远不需要定义域:只需挑出能描述该失败的那一个,然后 throw throws 它的某个值。

值类型 打印标签 code() 是 描述
::std::errc [posix] errno 值 POSIX / C 错误:invalid_argument、 no_such_file_or_directory、permission_denied、 no_space_on_device 等。这是本库的默认域。
::std::parse_errc [parse] 1 = 文件结束,2 = 部分,3 = 非法,4 = 溢出 文本解析与扫描失败,不涉及操作系统。
::std::cmath_errc [cmath] FE_* 标志位 浮点异常:invalid、divbyzero、 inexact、overflow、underflow。
::std::http_errc [http] HTTP 状态码 HTTP 响应:not_found(404)、im_a_teapot(418)、 internal_server_error(500)等。
::std::tls_alert [tls] (level << 8) | description TLS 警报:级别(warning/fatal)与描述 (certificate_expired、bad_record_mac 等)。
::std::win32_errc [win32] ERROR_* / GetLastError() 值 Windows API 失败。仅限 Windows 目标。
::std::nt_errc [nt] NTSTATUS / STATUS_* 值 NT 原生 API 失败。仅限 Windows 目标。
::std::com_errc [com] HRESULT 值 COM 失败。仅限 Windows 目标。
::std::wine_errc [wine] Wine 宿主侧的 errno 值 Wine 的 ntdll 报告的宿主错误。用于在 Wine 下运行的 Windows 目标。
::std::exception_ptr [itanium] / [msvc] 被装箱的旧式 EH 对象的身份 认出它然后继续:当仍在用旧式 C++ 异常的代码 跨越赫布异常边界时,到达的就是它。

该抛哪一个?在可移植代码中,用 ::std::errc。只有当编码确实来自 某个平台时,才使用该平台或协议的域:失败的 GetLastError() 是 win32_errc,来自对端的 TLS 警报是 tls_alert,你想向上传播的 HTTP 状态是 http_errc。不要手工翻译 —— 让 equivalent() 与 to_errc() 在需要 POSIX 语义的地方完成转换。

例如,下面三条语句都是合法的,而且不言自明:


throw throws ::std::errc::no_such_file_or_directory;
throw throws ::std::parse_errc::invalid;
throw throws ::std::http_errc::not_found;

8. 无处可逃:main 就是终点

抵达程序顶层的赫布异常没有接手的人,因此进程会陷入陷阱(trap)。 在 x86-64 上,检测到这种情况的指令会触发 SIGILL, 你得到的是核心转储,而不是一条消息:


$ ./a.out
Illegal instruction (core dumped)

这是刻意的:未被报告的错误就是 bug,而意识到 bug 最快的方式就是停下来。 由此产生的两个后果塑造了本教程中的每一个程序:

注意是哪个域作出了回答:程序要求一个数字,输入却不是数字, 于是失败是 [parse](3)Invalid format —— 而不是操作系统错误, 因为操作系统层面并没有出任何问题。

9. fast_io 在其中处于什么位置

fast_io 中每一个可能失败的操作都被声明为通过这个通道报告失败, 而每一个不可能失败的操作都被声明为 noexcept。你 永远不需要检查返回码,也永远不需要在调用之后检查 errno: 失败的调用会通过 throws 路径离开,并保持原来的 码空间不变。

这包括 I/O —— 正是因此 第2.14章从第一个程序起就把打印和扫描 包在 try { } catch throws (::std::error e) { } 中。 后续章节正是建立在这之上:文件、套接字、扫描器,一切都以这种方式 报告失败。

10. 常见错误

错误 编译器会说什么
在任何 try/处理程序链之外使用 throw throws 'throw throws' is only valid inside a function declared 'throws' or inside a 'try' block with a 'catch throws' handler
期望 throw throws 之后的那一行会执行 不会执行:该语句之后什么都不会运行。
catch throws (::std::errc e) 'catch throws' requires the caught type to be 'std::error' ('std::errc' given)
catch (::std::error e)(旧式处理程序) use of deleted function 'std::error::error(const std::error&)'
用 catch (...) { } 代替 catch throws an unhandled herbceptions failure cannot leave a function without a herbceptions specification
void f() noexcept throws 'throws'/'return_failure{E}' cannot be combined with 'noexcept'; the herbceptions specifier supersedes it
int main() throws '::main' cannot be declared with a herbceptions exception specification
忘记 -fherbceptions 在第一个 throws 处报 'throws' has not been declared
忘记 -lherbceptions 链接时对 __cxa_error_domain_* 报未定义引用

要点