Ch19.1: Defining and Overriding Error Domains
1. First: You Almost Certainly Do Not Need a Domain of Your Own
You already know how to throw and catch a herbception
(Ch3.16): every error is two words,
{domain, code}, and the domain is a pointer to a table of functions that
knows what the code means. This page is about the table itself, and the first rule is the
one that saves the most work:
Use a domain that already exists. A domain is not "the error type of one library". It is a code space, and one code space is enough for an entire program. The runtime ships nine of them, and between them they cover essentially everything a normal program can fail at:
| Domain type | Code space | Where it applies |
|---|---|---|
::std::errc |
POSIX errno values |
Everywhere. This is the default tongue of the platform layer |
::std::parse_errc |
ok, end_of_file, partial,
invalid, overflow |
Text-to-value conversion, in every format |
::std::http_errc |
HTTP status codes, the wire value itself | Clients and servers |
::std::cmath_errc |
FE_INVALID, FE_DIVBYZERO,
FE_INEXACT, FE_OVERFLOW, FE_UNDERFLOW |
Maths, where the answer is a value and a flag together |
::std::tls_alert |
TLS alert level and description | Any TLS connection |
::std::nt_errc |
NTSTATUS STATUS_* |
Windows kernels and native APIs |
::std::win32_errc |
Win32 GetLastError ERROR_* |
Win32 API |
::std::wine_errc |
Wine's own host errno space |
Wine's Unix side, where the values are not the host libc's |
::std::com_errc |
HRESULT |
COM and everything built on it |
The reason this works is the fourth function in the table below: every domain must be able
to answer "which ::std::errc is this?" That single obligation is what
makes error handling interoperable. The caller does not need to know which library produced
the failure, because a failure from any domain can be compared against
::std::errc::no_space_on_device and can be printed as a POSIX name and
message. This is the opposite of the old C++ std::error_code world, where every
library invented a std::error_category and no two of them could be compared.
So: a new domain is for an author of a library that owns a real code space —
a protocol, a file format, a device. If your code returns
::std::errc::invalid_argument and ::std::errc::value_too_large
like everybody else, you are done, and the page is over for you.
2. What a Domain Actually Is
::std::error_domain_singleton is a struct of five function pointers. That is
the whole ABI: the two-word error carries a pointer to one of these, and a code.
struct error_domain_singleton
{
void (*do_cleanup)(::std::size_t) noexcept = nullptr;
bool (*do_equivalent)(::std::size_t, ::std::error_domain_singleton const *,
::std::size_t) noexcept = nullptr;
void (*do_query_information)(::std::size_t, ::std::error_query_information,
::std::error_reporter_encoding, void *,
::std::error_reporter_io_cookie_function) noexcept = nullptr;
::std::errc (*do_to_errc)(::std::size_t) noexcept = nullptr;
void (*do_throw_legacy_exception)(::std::size_t,
::std::legacy_exception_abi) = nullptr;
};
Four of the five may be left nullptr. One may not.
| Hook | What it must do | If it is nullptr |
|---|---|---|
do_to_errc |
Translate your code into the nearest ::std::errc |
Mandatory. The compiler rejects a
throw throws whose domain has no do_to_errc, and
e.to_errc() is how every other library recovers your failure |
do_query_information |
Answer one of three questions about a code: name,
message or name_message |
Printing the error prints nothing. The value still works; you just cannot read it |
do_equivalent |
Say whether your code and another domain's code mean the same failure | e.equivalent(other) compares nothing. Compare
to_errc() instead, which is the default in the shipped domains |
do_cleanup |
Free whatever the code owns | Your codes own nothing. This is what you want whenever the code fits in
::std::size_t, which is every case in this chapter |
do_throw_legacy_exception |
Re-throw the code as a native C++ exception of the platform's own type | The runtime boxes the two words into
::std::cxx_std_error_legacy_exception instead
(Ch19.6) |
Note what is not in that struct: no name, no message string, no
what(), no base class, no type. A code is an integer and the table is
constant data, so a domain costs no allocation, no RTTI and no per-error object. The
information hook exists because text has to be produced somewhere, and it produces it
lazily, straight into the caller's output buffer, in the caller's encoding.
That last part is why the query hook takes an encoding and a callback rather
than returning a char const *: the same code can be printed as UTF-8, UTF-16,
UTF-32, GB18030 or EBCDIC into whatever stream asked, and the table is written once.
3. The Specialization: error_domain<T>
Users never touch the singleton directly. throw throws value asks two
questions of ::std::error_domain<T>: which singleton, and what code.
Specialize it once, next to the error type:
namespace std
{
template <>
class error_domain<::csv_errc>
{
public:
static inline constexpr ::std::error_domain_singleton const *domain() noexcept;
static inline constexpr ::std::size_t code(::csv_errc e) noexcept;
};
} // namespace std
Both functions must be static, inline, constexpr and
noexcept: domain() is intercepted by the constant evaluator, so a
throw throws inside a constexpr function works, and it is called
on every throw, so it must not do anything.
There is one more member you may provide, and no more:
static T from_std_error(::std::error const &). If it exists,
::std::herbception_cast<T>(e) uses it to turn a caught
::std::error back into your enum. Provide it when the translation from
errc is lossy and you want the original code back. Also, a domain may name a
domain_alias_type when several types share one singleton — then
is_code_of<T>() and == compare against the alias instead of
the type. Neither is needed for a first domain.
4. A Domain of Your Own
Here is the whole thing: a CSV reader with four failures. The type, two tables of text, one singleton, one specialization, and nothing else.
#include <fast_io.h>
#include <fast_io_dsal/string_view.h>
enum class csv_errc : ::std::uint_least32_t
{
ok = 0,
unterminated_quote = 1,
field_too_long = 2,
too_many_fields = 3
};
namespace csv_details
{
inline constexpr ::fast_io::string_view names[]{
"csv:ok",
"csv:unterminated_quote",
"csv:field_too_long",
"csv:too_many_fields"};
inline constexpr ::fast_io::string_view messages[]{
"the record parsed",
"a quoted field reached the end of the record",
"a field is longer than the declared limit",
"the record has more fields than the header"};
inline constexpr ::std::error_domain_singleton singleton{
.do_equivalent = [](::std::size_t code, ::std::error_domain_singleton const *other,
::std::size_t other_code) noexcept -> bool {
return singleton.do_to_errc(code) == other->do_to_errc(other_code);
},
.do_query_information = [](::std::size_t code, ::std::error_query_information query,
::std::error_reporter_encoding, void *cookie,
::std::error_reporter_io_cookie_function report) noexcept {
if (code >= ::std::size(names))
{
return;
}
::std::io_scatter_t const pieces[3]{
{names[code].data(), names[code].size()},
{static_cast<void const *>(": "), 2},
{messages[code].data(), messages[code].size()}};
switch (query)
{
case ::std::error_query_information::name:
report(cookie, pieces, 1);
break;
case ::std::error_query_information::message:
report(cookie, pieces + 2, 1);
break;
default:
report(cookie, pieces, 3);
break;
}
},
.do_to_errc = [](::std::size_t code) noexcept -> ::std::errc {
switch (static_cast<csv_errc>(code))
{
case csv_errc::unterminated_quote:
return ::std::errc::invalid_argument;
case csv_errc::field_too_long:
case csv_errc::too_many_fields:
return ::std::errc::value_too_large;
default:
return ::std::errc{};
}
}};
} // namespace csv_details
namespace std
{
template <>
class error_domain<::csv_errc>
{
public:
static inline constexpr ::std::error_domain_singleton const *domain() noexcept
{
return __builtin_addressof(::csv_details::singleton);
}
static inline constexpr ::std::size_t code(::csv_errc e) noexcept
{
return static_cast<::std::size_t>(static_cast<::std::uint_least32_t>(e));
}
};
} // namespace std
int main()
{
try
{
throw throws csv_errc::unterminated_quote;
}
catch throws (::std::error e)
{
::fast_io::io::print("name ", ::fast_io::mnp::name(e), "\n"
"message ", ::fast_io::mnp::message(e), "\n"
"name_message ", e, "\n"
"as posix ", ::fast_io::mnp::posix_equivalent_name_message(e), "\n"
"equivalent ", ::fast_io::mnp::boolalpha(e.equivalent(::std::errc::invalid_argument)), "\n"
"is struct ", ::fast_io::mnp::boolalpha(e.is_code_of<::std::errc>()), "\n");
}
}
name csv:unterminated_quote
message a quoted field reached the end of the record
name_message csv:unterminated_quote: a quoted field reached the end of the record
as posix [posix](22)Invalid argument
equivalent true
is struct false
Read the last two lines together. equivalent is true because
do_equivalent asked do_to_errc about both sides, and
is_code_of<::std::errc>() is false because the domain is
still yours: equivalent is about meaning, identity is about the pointer. Both
answers are useful, and they are different questions.
The boolalpha is not decoration: println of a bool
prints 0 and 1 unless you say otherwise.
The report(cookie, pieces, 3) call is the whole printing protocol. The cookie
is an opaque handle to the caller's output; the scatters are
{pointer, length} pairs, so no temporary string is built and no
char const * with an assumed NUL is involved. Give three pieces for
name_message, and one each for name and message
— that is the entire contract, and the same code then prints to a console, a file, a
socket or a buffer.
5. Replacing a Shipped Domain
Sometimes the shipped table is wrong for your environment: a freestanding runtime with its
own error numbering, a bare-metal board whose errno values are not the host's,
a test harness that wants to prove the error path was taken, a platform where the message
text must be replaced by your own language. The predefined domains live behind five-line
extern "C" entry points, and on ELF they are weak symbols
— nm -D on our runtime shows them as W. Define the same
symbol in your own binary and yours is the one that is used:
#include <fast_io.h>
#include <fast_io_dsal/string_view.h>
/* one environment, one table: the default posix domain does not apply here */
namespace
{
inline constexpr ::fast_io::string_view host_messages[]{
"no error",
"operation not permitted",
"no such file or directory"};
inline constexpr ::std::error_domain_singleton host_posix{
.do_query_information = [](::std::size_t code, ::std::error_query_information,
::std::error_reporter_encoding, void *cookie,
::std::error_reporter_io_cookie_function report) noexcept {
if (code >= ::std::size(host_messages))
{
return;
}
::std::io_scatter_t const piece{host_messages[code].data(), host_messages[code].size()};
report(cookie, __builtin_addressof(piece), 1);
},
.do_to_errc = [](::std::size_t code) noexcept -> ::std::errc {
return static_cast<::std::errc>(code);
}};
} // namespace
extern "C" ::std::error_domain_singleton const *__cxa_error_domain_posix() noexcept
{
return __builtin_addressof(host_posix);
}
int main()
{
try
{
throw throws ::std::errc::no_such_file_or_directory;
}
catch throws (::std::error e)
{
::fast_io::io::print("message ", ::fast_io::mnp::message(e), "\n");
}
}
message no such file or directory
Nothing else changed: the program still throws ::std::errc, and the compiler
still emits a two-word channel. Only the table behind the pointer moved. That is the
advantage of putting the behaviour in a table instead of in the type: a portal can replace
the environment's error model without touching a single line of the code that reports
errors.
Do this when the environment genuinely differs, and do not do it to "customise the wording" of an ordinary program — the shipped domains carry the platform's own messages in the platform's own language, which is almost always the right answer.
6. Windows: One Runtime for Two C++ ABIs
The same table-based design is why the Windows runtime is a plain
herbceptions.dll that both toolchains can link. Its export table is C names
only:
__cxa_error_code_msvc_exception_ptr
__cxa_error_code_msvc_exception_ptr_cast
__cxa_error_code_msvc_exception_ptr_unbox
__cxa_error_domain_cmath
__cxa_error_domain_com
__cxa_error_domain_http
__cxa_error_domain_msvc_exception_ptr
__cxa_error_domain_nt
__cxa_error_domain_parse
__cxa_error_domain_posix
__cxa_error_domain_tls
__cxa_error_domain_win32
__cxa_error_domain_wine
__cxa_error_query_information_clone
__cxa_error_query_information_create
__cxa_error_query_information_free
No mangled C++ names, no std::string, no std::exception in the
interface: ::std::error is two words, so the boundary is two words. A
MinGW-GCC-built binary and an MSVC-built binary can both link the same DLL and throw into
each other's code. That is exactly what the Itanium/MSVC split cannot do for
std::exception — which is why the runtime also carries
::std::legacy_exception_abi, with itanium,
microsoft and platform values, so a legacy-exception bridge knows
which ABI it is bridging to (Ch19.6).
7. Checklist Before You Add a Domain
- Can
::std::errcexpress it? Then use::std::errcand stop. - Is it parsing?
::std::parse_errcexists. HTTP?::std::http_errc. Maths?::std::cmath_errc. TLS?::std::tls_alert. - Are you writing a library with a code space nobody else has — a protocol, a format, a device? Then one domain for the whole library, not one per class and not one per function.
- Implement
do_to_errcfirst and make it total: every code must map to something sensible, because that mapping is what the rest of the world sees. - Keep the codes dense integers and keep the tables
constexpr. A domain with a heap allocation in it is a domain that can fail while reporting a failure. - Leave
do_cleanupnull unless the code really owns memory. - Test that
e.equivalent(::std::errc::...)answers the way you claim, and that printing works in every encoding you ship.