第19.5章:<system_error>
1. 本应修好错误码的那个头文件
加入 <system_error> 是为了给 C++ 一个可移植地携带操作系统错误码的方式。它定义了四样东西:
| 类型 | 它是什么 |
|---|---|
::std::errc |
一组 POSIX 错误值的 enum class:invalid_argument、no_space_on_device、not_enough_memory 等等。这一部分是好的,而且活了下来
|
::std::error_code |
一个值,外加一个指向说明该值含义的 error_category 的指针
|
::std::error_condition |
同样的形状,用于"这一类错误"而不是某个具体错误 |
::std::error_category |
让错误码有含义的东西:一个抽象类,你要从它派生并注册成单例 |
这个设计听起来应该很熟悉:它就是赫布异常错误域的直接祖先。区别在于含义是如何携带的 —— 而这个区别正是本页的全部故事。
2. 用法
#include <fast_io.h>
#include <system_error>
int main()
{
try
{
throw ::std::system_error(::std::make_error_code(::std::errc::no_space_on_device));
}
catch (::std::system_error const &e)
{
::fast_io::io::print("what ", ::fast_io::mnp::os_c_str(e.what()), "\n"
"code value ", e.code().value(), "\n"
"category ", ::fast_io::mnp::os_c_str(e.code().category().name()), "\n"
"message ", ::fast_io::mnp::os_c_str(e.code().message().c_str()), "\n"
"generic? ", ::fast_io::mnp::boolalpha(e.code() == ::std::errc::no_space_on_device), "\n");
}
}
what No space left on device
code value 28
category generic
message No space left on device
generic? true
注意程序第四行里的 .c_str():message() 按值返回一个 ::std::string。每次调用它都会分配一个新字符串、把消息格式化进去、再交回来 —— 在失败路径上,而那正是你已经缺资源的唯一一条路径。赫布异常世界里的同一次调用是 ::fast_io::mnp::message(e),它把 scatter 直接写进输出缓冲区,一次分配都不做(第19.1章)。
::std::system_error 本身就是一个遗留异常:它派生自 ::std::runtime_error,所以带着一个 what() 字符串,同时还存着一个 error_code。在 libstdc++ 里它的构造函数真的会调用 runtime_error(__ec.message()) —— 一次分配用于错误域里的消息字符串,另一次用于异常对象内部的拷贝,而这一切都发生在抛出还没开始之前。
3. 错误域是一个带 vtable 的对象
这是 libstdc++ 的 <system_error> 里的接口。要把它读成一张义务清单:每个想拥有自己错误码的库都必须履行这些义务:
class error_category
{
public:
constexpr error_category() noexcept = default;
virtual ~error_category();
error_category(const error_category&) = delete;
error_category& operator=(const error_category&) = delete;
/// A string that identifies the error category.
virtual const char*
name() const noexcept = 0;
// We need two different virtual functions here, one returning a
// COW string and one returning an SSO string. Their positions in the
// vtable must be consistent for dynamic dispatch to work, but which one
// the name "message()" finds depends on which ABI the caller is using.
private:
_GLIBCXX_DEFAULT_ABI_TAG
virtual __cow_string
_M_message(int) const;
public:
/// A description of the error condition corresponding to the number.
_GLIBCXX_DEFAULT_ABI_TAG
virtual string
message(int) const = 0;
public:
/// Return an error_condition corresponding to `i` in this category.
virtual error_condition
default_error_condition(int __i) const noexcept;
/// Test whether `cond` is equivalent to `i` for this category.
virtual bool
equivalent(int __i, const error_condition& __cond) const noexcept;
/// Test whether `code` is equivalent to `i` for this category.
virtual bool
equivalent(const error_code& __code, int __i) const noexcept;
/// An error_category only compares equal to itself.
[[__nodiscard__]]
bool
operator==(const error_category& __other) const noexcept
{ return this == &__other; }
那个声明里有四件事决定了这个设计的命运:
-
它是一个带虚函数的类。错误域是一个对象,带 vtable、析构函数和类型 —— 于是错误域只能是藏在指针后面的单例,而
operator==比较的是this == &__other。两个定义了同名错误域的库拥有两个不同的错误域,它们的错误码永远不会相等。 -
message()返回::std::string。生成错误的文本要分配内存。你没有办法要求另一种编码的形式,也没有办法写进你已有的缓冲区。 -
有两个层次:
error_code和error_condition。"这个具体失败"和"这一类失败"之间的差别是真实的,但把一个和另一个比较的规则是default_error_condition加上两个equivalent重载,几乎没人能凭记忆说清它们。把它们写错的代码照样能编译。 -
声明中间的那段注释是一道 ABI 伤疤。返回
__cow_string的_M_message和返回string的message并排存在,是因为改变虚函数的返回类型会改变 vtable,而标准库不能破坏旧 ABI。一个自己的接口被迫绕着五年前的 ABI 错误分层的错误处理设施,是不可能被修好的。
4. 它在实践中为何失败
每个想要错误码的库都定义了自己的错误域,结果正是这个特性本应终结的那种碎片化:来自网络库、文件系统库和数据库库的 error_code 是三个不可比较的值,带着三张消息表、三个单例。一个返回 ::std::error_code 的函数对调用者什么也没说 —— 不说它会在哪个错误域里,所以调用者甚至无法在不了解那个库的情况下把它打印出来;而把它与 ::std::errc::no_space_on_device 比较,只有在那个特定库记得走通用错误域时才成立。
赫布异常的错误域保住了那唯一的好想法,并删掉了让碎片化变得容易的机制。要紧的差别如下:
<system_error> |
赫布异常错误域 |
|---|---|
| A category is a class with virtual functions and a vtable |
错误域是一个由五个函数指针组成的 constexpr 结构体 —— ::std::error_domain_singleton。没有对象、没有 vtable、没有析构函数
|
message() returns ::std::string |
do_query_information 把 {指针, 长度} 形式的 scatter 写进调用者的缓冲区,用调用者的编码,不做任何分配
|
Meaning is compared through default_error_condition and two
equivalent overloads nobody remembers |
只有一个必需的钩子:do_to_errc。每个错误域都必须回答"这是哪一个 ::std::errc",而那个答案正是所有人用来比较的东西
|
An error is an object: error_code plus a message string plus, in
system_error, a whole exception hierarchy |
错误是两个字:{domain 指针, 错误码}。它满足 sizeof(::std::error) == 2 * sizeof(void *),并且走返回寄存器
|
Carrying an error to the caller requires a legacy throw |
把错误带到调用者那里就是返回路径本身;不分配任何东西,也不遍历栈 |
::std::errc 是活下来并原样保留的那部分,它成了 POSIX 错误域。当你在 fast_io 或本书里看到 ::std::errc::not_enough_memory 时,它就是同一个枚举:那些值一直是对的,错的是它们周围的机制。
5. 在别人的代码里怎么读它
-
接口里的
std::error_code意味着作者想在没有异常的世界里报告失败,而那个世界没有更好的办法。他真正想要的是::std::error。 -
自定义的
std::error_category就是一个带 vtable 的错误域。读它的message()表来了解错误码,然后在边界处转换(第19.6章)。 -
std::system_error是一个遗留异常。它会展开你的栈、分配两次、然后在处理块里到达 —— 而那个处理块不通过.code()就拿不到错误码。这就是为什么它是<filesystem>的失败类型,也是为什么那个库在任何没有 unwinder 的目标上都很难用。