第3.16章: 赫布异常(Herbceptions)
概述
在之前的每一页中,我们都默默地假定没有任何操作会失败。从这里开始,这种沉默结束了: fast_io 中的每一个错误都是赫布异常(herbception),而 赫布异常是本教程中唯一的错误报告机制。
struct raw_herbception)、编译器选项
(-fherbceptions、-lherbceptions)以及编译器输出的
英文诊断信息,一律保留英文原名,不做翻译。
你只需要学两条语句:
throw throws value;—— 报告失败try { } catch throws (::std::error e) { }—— 处理失败
读懂这一页不需要任何别的知识:下面每个例子都是一个单独的 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。没有第二条返回路径,也不需要任何设置:
- 没有栈展开,没有
.eh_frame表,没有 personality 例程 - 永远不为错误做堆分配
- 没有
std::exception基类,没有what(),没有 RTTI
赫布异常在两条路径上都非常非常廉价。用这种方式报告错误的函数, 生成的代码与返回错误码的函数相同,而失败路径只是返回两个字的普通返回。 永远不要为了“速度”而选择“返回错误码再检查”的风格, 也永远不要因为“抛异常听起来很贵”而不去报告失败。
2. 构建:-fherbceptions -lherbceptions
赫布异常是一项编译器特性,而不是库的小把戏,所以你需要一个实现了它的编译器。 只有我们的 GCC 与 LLVM 构建可以:
- GCC —— 我们的赫布异常版 GCC(
g++17+)。 - Clang —— 我们的赫布异常版 LLVM(
clang++24+)。 -
不支持 MSVC:
cl.exe没有-fherbceptions,因此本教程不用它构建。Windows 目标没问题 —— 用我们的 Clang 或我们的 MinGW GCC。
两个选项,这就是全部额外的添加:
-
-fherbceptions打开这项语言特性。它必须在编译每一个 翻译单元时传入,而不只是在链接时。 -
-lherbceptions链接运行时,它拥有这些错误域 (负责命名并打印错误的代码)。
# 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
三条规则,全部由编译器强制执行:
-
它必须在能处理该失败的地方。在这里它位于一个带有
catch throws的try内。把它放到别处,编译器会说:
(等你在第6章中编写自己的函数后, 同样的语句会出现在函数内部,并且函数会被声明为允许错误离开。 语句本身没有任何变化。)error: 'throw throws' is only valid inside a function declared 'throws' or inside a 'try' block with a 'catch throws' handler -
它之后的代码都不会执行。在示例中,是处理程序里的
return 1;结束了程序;throw throws之后的那一行永远不会执行。 -
重新抛出是
throw throws;—— 不带操作数 —— 并且只能在catch throws处理程序内。它把同一个错误 交给外层处理程序:#include <fast_io.h> using namespace ::fast_io::io; int main() { try { try { throw throws ::std::errc::permission_denied; } catch throws (::std::error e) { perr("inner handler: ", e, "\n"); throw throws; // rethrow: pass it to the enclosing handler } } catch throws (::std::error e) { perr("outer handler: ", e, "\n"); return 1; } return 0; }inner handler: [posix](13)Permission denied outer handler: [posix](13)Permission denied
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 */
}
规则同样由编译器强制执行:
-
处理程序按值声明
::std::error。不能按引用, 不能加const&,也不能是::std::errc:error: 'catch throws' requires the caught type to be 'std::error' ('std::errc' given) -
你不能存储错误。
::std::error没有默认构造函数, 也不能拷贝;只有编译器能创建它,并且处理程序拥有它。 你需要什么就在处理程序体内就地读取。 -
处理程序可以无名:
catch throws (::std::error) { }。当你唯一能做的就是 跳过失败的操作继续执行时,用它。 -
catch (...)不会捕获赫布异常,而且在本教程中 被禁止:失败只能由catch throws块来处理。 -
不带
throws的catch (::std::error e)也不 合法。那是旧式处理程序,它无法编译:
旧式 C++ 异常不属于本教程的一部分。error: use of deleted function 'std::error::error(const std::error&)'
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
perrln(e)—— 完整内容,单独一行,写到 stderr。::fast_io::mnp::name(e)—— 只要域名。::fast_io::mnp::message(e)—— 编码与文本,不含域名。-
::fast_io::mnp::posix_equivalent_name(e)/::fast_io::mnp::posix_equivalent_message(e)—— 对非 POSIX 错误的 POSIX 等价物给出同样的内容,这样日志行始终以errc说话。
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 最快的方式就是停下来。 由此产生的两个后果塑造了本教程中的每一个程序:
-
main不能被声明为throws。 编译器会拒绝,因为已经没有调用者能处理该错误了:error: '::main' cannot be declared with a herbceptions exception specification -
所以
main负责捕获、报告,并把失败转成 退出状态。这正是本教程每个示例的形态:#include <fast_io.h> using namespace ::fast_io::io; int main() { ::std::int_least32_t value{}; try { scan(::fast_io::c_stdin(), value); println("you typed: ", value); } catch throws (::std::error e) { perrln(e); return 1; } return 0; }$ echo abc | ./a.out [parse](3)Invalid format $ echo 42 | ./a.out you typed: 42
注意是哪个域作出了回答:程序要求一个数字,输入却不是数字,
于是失败是 [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_* 报未定义引用 |
要点
- fast_io 中的所有错误都是赫布异常。本教程中没有需要检查的 错误码 API,也没有旧式 C++ 异常处理。
-
每个翻译单元都用
-fherbceptions构建,链接时加-lherbceptions,并使用我们的 GCC 或 LLVM 构建。MSVC 做不到; 在 Windows 上请使用我们的 Clang 或我们的 MinGW GCC。 -
用
throw throws value;报告,用try { } catch throws (::std::error e) { }处理,用throw throws;转发。 -
处理程序按值声明
::std::error,错误不能被拷贝或存储: 你需要什么就在处理程序内部读取。 -
可以向错误询问:
domain()、code()、==、equivalent()、to_errc()、is_code_of<T>()。 -
可移植代码中抛
::std::errc;当失败确实属于parse_errc、cmath_errc、http_errc、tls_alert以及各 Windows 域时,就用它们。 - 赫布异常是返回寄存器中紧挨条件标志的两个字,因此它在两条路径上都很廉价。 不要围绕它的开销来做设计。
-
未被捕获的赫布异常会触发陷阱,而
main不能是throws—— 所以由main捕获、报告,并返回 退出状态。