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:
-
It is a class with virtual functions. A category is an object, with a
vtable, a destructor and a type — so categories are singletons behind pointers, and
operator==comparesthis == &__other. Two libraries that define a category with the same name have two different categories, and their codes never compare equal. -
message()returns::std::string. Producing the text of an error allocates. There is no way to ask for it in another encoding, or to write it into a buffer you already have. -
There are two levels:
error_codeanderror_condition. The difference between "this specific failure" and "a failure of this kind" is real, but the rules for comparing one to the other aredefault_error_conditionplus twoequivalentoverloads, and almost nobody can state them from memory. Code that gets them wrong compiles. -
The comment in the middle of the declaration is an ABI scar.
_M_messagereturning__cow_stringnext tomessagereturningstringexists because changing the return type of a virtual function changes the vtable, and the standard library could not break the old ABI. An error-handling facility whose own interface is layered around a five-year-old ABI mistake is not a facility that can be fixed.
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
-
std::error_codein an interface means the author wanted to report a failure without an exception, in a world that had no better way. What they wanted was::std::error. -
A custom
std::error_categoryis a domain with a vtable. Read itsmessage()table to learn the codes, then convert at the boundary (Ch19.6). -
std::system_erroris a legacy exception. It will unwind your stack, allocate twice, and arrive at a handler that cannot get the code out without.code()— which is why it is the failure type of<filesystem>, and why that library is difficult to use on any target without an unwinder.