Ch3.16: Herbceptions

中文(简体)

Overview

Every page before this one quietly assumed that nothing can fail. From here on nothing is quiet: every error in fast_io is a herbception, and herbceptions are the only error-reporting mechanism in this tutorial.

Two statements are all you have to learn:

You do not need to know anything else to read this page: every example below is a single int main() that throws an error and catches it again.

1. What a Herbception Is

A herbception is a two-word error value:


struct raw_herbception
{
	::std::error_domain_singleton const *domain; // which error code-space
	::std::size_t code;                          // the code inside that space
};

That is sizeof(::std::error) == 2 * sizeof(void *) whatever the error is. It travels back to the caller in the return registers, next to a single failure bit in the condition register — CF on x86, NZCV.C on AArch64. There is no second return path and nothing to set up:

Herbceptions are very, very cheap on both paths. A function that reports errors this way produces the same code as one returning an error code, and the failure path is a plain return of two words. Never choose a “return an error code and check it” style “for speed”, and never avoid reporting a failure because throwing sounds expensive.

2. Building: -fherbceptions -lherbceptions

Herbceptions are a compiler feature, not a library trick, so you need a compiler that implements them. Only our GCC and LLVM builds do:

Two flags, and that is the whole addition:


# our Clang
clang++ --config=$HOME/cfgs/x86_64-linux-gnu-libcxx.cfg \
  -O3 -flto=thin -fherbceptions -I$HOME/libraries/fast_io/include \
  hello.cc -lherbceptions -o hello

# our GCC
g++ -std=c++26 -O3 -s -fuse-ld=lld -fherbceptions \
  -I$HOME/libraries/fast_io/include \
  hello.cc -lherbceptions -o hello

On Windows the same flags work with a Windows target, and --sysroot finds the runtime, so there is no -L to write:


clang++ --config=$HOME/cfgs/x86_64-windows-msvc.cfg \
  -O3 -flto=thin -fherbceptions -I$HOME/libraries/fast_io/include \
  hello.cc -lherbceptions -o hello.exe

x86_64-w64-mingw32-g++ -std=c++26 -O3 -s -fherbceptions \
  -I$HOME/libraries/fast_io/include \
  hello.cc -lherbceptions -lntdll -o hello.exe

Forget -fherbceptions and the compiler rejects the syntax; forget -lherbceptions and the link fails on the domain symbols. Both are covered in Ch1.8.

3. The throw throws Statement

Reporting a failure is a statement, and it has one shape:


throw throws expression;

throws is the marker saying “this error leaves through the herbception channel”. The expression is a value of a type that has an error domain — in practice one of the standard error enums listed in section 7.


#include <fast_io.h>

using namespace ::fast_io::io;

int main()
{
	try
	{
		throw throws ::std::errc::invalid_argument;
	}
	catch throws (::std::error e)
	{
		perrln(e);
		return 1;
	}
	return 0;
}

Output, and the exit status the program returns:


[posix](22)Invalid argument
exit status: 1

Three rules, all enforced by the compiler:

Check first, then report. Test the condition at the top of the block and throw throws immediately, before anything has been written or printed. That is how fast_io itself is written.

4. The try { } catch throws (::std::error e) { } Statement

Handling is a statement too, with exactly one shape:


try
{
	/* anything that may fail */
}
catch throws (::std::error e)
{
	/* inspect / report / recover / rethrow */
}

The rules, again enforced by the compiler:

5. ::std::error: The Members You Use

The value inside the handler is tiny: two words and six things you can ask it. This is the complete list you need.

Expression Result Meaning
e.domain() ::std::error_domain_singleton const * Which code-space the error belongs to. Never null inside a handler.
e.code() ::std::size_t The raw code inside that domain — for POSIX errors, the errno value.
e == ::std::errc::permission_denied bool Exact match: same domain and same code. The type of the value on the right picks the domain.
e.equivalent(::std::errc::no_such_file_or_directory) bool Cross-domain match: does this error mean the same thing as that code? An HTTP 404 equals ENOENT this way.
e.to_errc() ::std::errc The POSIX equivalent of the error, as a ::std::errc.
e.is_code_of<::std::http_errc>() bool “Was this error produced by this domain?”, ignoring the code.

#include <fast_io.h>

using namespace ::fast_io::io;

int main()
{
	try
	{
		throw throws ::std::http_errc::not_found;
	}
	catch throws (::std::error e)
	{
		println("domain is http:  ", e.is_code_of<::std::http_errc>());
		println("domain is posix: ", e.is_code_of<::std::errc>());
		println("== not_found:    ", e == ::std::http_errc::not_found);
		println("== errc ENOENT:  ", e == ::std::errc::no_such_file_or_directory);
		println("equivalent:      ", e.equivalent(::std::errc::no_such_file_or_directory));
		println("to_errc:         ", static_cast<::std::int_least32_t>(e.to_errc()));
		println("code:            ", e.code());
	}
}

domain is http:  1
domain is posix: 0
== not_found:    1
== errc ENOENT:  0
equivalent:      1
to_errc:         2
code:            404

Look at == and equivalent together. The error is http_errc::not_found, so it is not an ENOENT (different domain: == is false), but it means the same thing (so equivalent is true). That difference is the whole reason both members exist.

6. Printing an Error

Because an error is just a domain and a code, fast_io can print it. The full form is [domain](code)message, and you can ask for pieces of it:


#include <fast_io.h>

using namespace ::fast_io::io;

int main()
{
	try
	{
		throw throws ::std::errc::no_space_on_device;
	}
	catch throws (::std::error e)
	{
		perrln(e);                                          // the whole line, to stderr
		perr("inside a line:  ", e, "\n");
		perr("domain name:    ", ::fast_io::mnp::name(e), "\n");
		perr("code + text:    ", ::fast_io::mnp::message(e), "\n");
		perr("as posix name:  ", ::fast_io::mnp::posix_equivalent_name(e), "\n");
		perr("as posix text:  ", ::fast_io::mnp::posix_equivalent_message(e), "\n");
	}
}

[posix](28)No space left on device
inside a line:  [posix](28)No space left on device
domain name:    posix
code + text:    (28)No space left on device
as posix name:  posix
as posix text:  [posix](28)No space left on device
perr writes to stderr, print to stdout. Error reports go to stderr in this tutorial, so a program whose output is redirected to a file still shows its failures on the terminal. Reporting an error is an ordinary I/O call: nothing special has to be wrapped around it.

7. The Error Domains

A domain is a code-space. Each one is a type with an error-domain specialization and its own way of packing code(). These are all of them. You never define a domain: you pick the one that describes the failure and throw throws one of its values.

Value type Printed tag code() is Describes
::std::errc [posix] the errno value POSIX / C errors: invalid_argument, no_such_file_or_directory, permission_denied, no_space_on_device, ... The default domain of the library.
::std::parse_errc [parse] 1 = end of file, 2 = partial, 3 = invalid, 4 = overflow Text parsing and scanning failures, with no OS involved.
::std::cmath_errc [cmath] the FE_* flag bits Floating-point exceptions: invalid, divbyzero, inexact, overflow, underflow.
::std::http_errc [http] the HTTP status code HTTP responses: not_found (404), im_a_teapot (418), internal_server_error (500), ...
::std::tls_alert [tls] (level << 8) | description TLS alerts: level (warning/fatal) and description (certificate_expired, bad_record_mac, ...).
::std::win32_errc [win32] the ERROR_* / GetLastError() value Windows API failures. Windows targets only.
::std::nt_errc [nt] the NTSTATUS / STATUS_* value NT native API failures. Windows targets only.
::std::com_errc [com] the HRESULT value COM failures. Windows targets only.
::std::wine_errc [wine] Wine’s host-side errno values Host errors reported by Wine’s ntdll. Windows targets running under Wine.
::std::exception_ptr [itanium] / [msvc] an identity for a boxed legacy-EH object Recognise it and move on: it is what arrives when code that still uses legacy C++ exceptions crosses a herbception boundary.

Which one to throw? In portable code, ::std::errc. Use a platform or protocol domain only when the code really came from that platform: a failed GetLastError() is a win32_errc, a TLS alert from the peer is a tls_alert, an HTTP status you want to propagate is an http_errc. Do not translate by hand — equivalent() and to_errc() do it at the point where somebody needs POSIX semantics.

For example, these three statements are legal and self-explanatory:


throw throws ::std::errc::no_such_file_or_directory;
throw throws ::std::parse_errc::invalid;
throw throws ::std::http_errc::not_found;

8. Nothing Escapes: main Is Where It Stops

A herbception that reaches the top of the program has nobody to be handed to, so the process traps. On x86-64 the instruction that detects the situation raises SIGILL, and you get a core dump rather than a message:


$ ./a.out
Illegal instruction (core dumped)

That is deliberate: an unreported error is a bug, and the quickest way to notice a bug is to stop. Two consequences shape every program in this tutorial:

Note which domain answered: the program asked for a number, the input was not one, and the failure is [parse](3)Invalid format — not an OS error, because nothing about the operating system went wrong.

9. Where fast_io Fits In

Every operation in fast_io that can fail is declared so that it reports through this channel, and every operation that cannot fail is declared noexcept. You never check a return code and you never inspect errno after a call: a call that fails leaves through the throws path with the original code-space intact.

That includes I/O, which is why Ch2.14 wrapped its printing and scanning in try { } catch throws (::std::error e) { } from the first program on. The next chapters build on exactly that: files, sockets, scanners, everything reports failure this way.

10. Common Mistakes

Mistake What the compiler says
throw throws outside any try/handler chain 'throw throws' is only valid inside a function declared 'throws' or inside a 'try' block with a 'catch throws' handler
Expecting the line after throw throws to run It does not: nothing after the statement executes.
catch throws (::std::errc e) 'catch throws' requires the caught type to be 'std::error' ('std::errc' given)
catch (::std::error e) (legacy handler) use of deleted function 'std::error::error(const std::error&)'
catch (...) { } instead of catch throws an unhandled herbceptions failure cannot leave a function without a herbceptions specification
void f() noexcept throws 'throws'/'return_failure{E}' cannot be combined with 'noexcept'; the herbceptions specifier supersedes it
int main() throws '::main' cannot be declared with a herbceptions exception specification
forgetting -fherbceptions 'throws' has not been declared, at the first throws
forgetting -lherbceptions undefined reference to __cxa_error_domain_* at link time

Key Takeaways