第19.5章:<system_error>

English

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; }

那个声明里有四件事决定了这个设计的命运:

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. 在别人的代码里怎么读它