Ch19.5: <system_error>

中文(简体)

1. The Header That Was Supposed to Fix Error Codes

<system_error> was added to give C++ a portable way to carry an error code from the operating system. It defines four things:

Type What it is
::std::errc An enum class of POSIX error values: invalid_argument, no_space_on_device, not_enough_memory, and so on. This part is good, and it survived
::std::error_code A value plus a pointer to an error_category that says what the value means
::std::error_condition The same shape, for "some error of this kind" rather than one specific error
::std::error_category The thing that gives codes their meaning: an abstract class you derive from and register as a singleton

If that design sounds familiar, it should: it is the direct ancestor of the herbception error domain. The difference is how the meaning is carried — and the difference is the whole story of this page.

2. Using It


#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

Note the .c_str() on line four of the program: message() returns a ::std::string by value. Every call to it allocates a fresh string, formats the message into it, and hands it back — on the error path, which is the one path where you are already short of resources. The same call in the herbception world is ::fast_io::mnp::message(e), which writes scatters straight into the output buffer and allocates nothing (Ch19.1).

::std::system_error itself is a legacy exception: it derives from ::std::runtime_error, so it carries a what() string, and it also stores an error_code. In libstdc++ its constructor literally calls runtime_error(__ec.message()) — one allocation for the message string inside the category, and another for the copy inside the exception object, before the throw has even started.

3. The Category Is an Object With a Vtable

Here is the interface, from libstdc++'s <system_error>. Read it as a list of obligations that every library which wants its own error codes has to discharge:


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

Four things in that declaration decided the fate of the design:

4. Why It Failed in Practice

Every library that wanted error codes defined its own category, and the result is exactly the fragmentation the feature was supposed to end: an error_code from a networking library, a filesystem library and a database library are three incomparable values with three message tables and three singletons. A function that returns ::std::error_code tells its caller nothing about which category it will be in, so the caller cannot even print it without knowing the library, and comparing it to ::std::errc::no_space_on_device works only if that particular library remembered to route through the generic category.

The herbception domain keeps the one good idea and deletes the mechanism that made fragmentation easy. The differences that matter:

<system_error> Herbception domain
A category is a class with virtual functions and a vtable A domain is a constexpr struct of five function pointers — ::std::error_domain_singleton. No object, no vtable, no destructor
message() returns ::std::string do_query_information writes {pointer, length} scatters into the caller's buffer, in the caller's encoding, with no allocation
Meaning is compared through default_error_condition and two equivalent overloads nobody remembers One mandatory hook: do_to_errc. Every domain must answer "which ::std::errc is this?", and that answer is what everybody compares
An error is an object: error_code plus a message string plus, in system_error, a whole exception hierarchy An error is two words: {domain pointer, code}. It fits in sizeof(::std::error) == 2 * sizeof(void *), and it flows through the return registers
Carrying an error to the caller requires a legacy throw Carrying an error is the return path itself; nothing is allocated and no stack is walked

::std::errc is the part that survived, unchanged, and became the POSIX error domain. When you see ::std::errc::not_enough_memory in fast_io or in this book, it is that same enum: the values were always right; the machinery around them was not.

5. How to Read It in Someone Else's Code