Ch20.10: Herbceptions: the Only Error Handling That Exists Here

中文(简体)

1. The Same Feature, Implemented the Other Way

A herbception is two words on the return path and a failure bit: the error value is {domain pointer, code}, and the calling convention says whether it is present (3.16). There is no object to allocate, no stack to walk, no table to register and no personality routine, because nothing is being unwound — the function returns, exactly like a function that returns an error code.

Everything that made legacy exceptions impossible freestanding is therefore absent from the mechanism by construction. What is left is one question: the error domain has to know what its codes mean, and that knowledge lives in a library. So how does that library stay freestanding?

2. Measured: One Symbol Instead of Eleven

The same throw-and-catch program as 20.9, written with the mechanism this book uses:

#include <herbceptions/error>

void fail(int x) throws { if (x < 0) { throw throws ::std::errc::invalid_argument; } }

int main() { try { fail(-1); } catch throws (::std::error) { return 0; } return 1; }
$ g++ -std=c++26 -fherbceptions -O2 -c herb.cc && nm -u herb.o
                 U __cxa_error_domain_posix
  [1 undefined symbol]
$ size legacy.o herb.o
   text	   data	    bss	    dec	    hex	filename
    452	      0	      0	    452	    1c4	legacy.o
    230	      0	      0	    230	     e6	herb.o

One symbol, and it is a data pointer to the error domain singleton — not a function, not a runtime, not a table that a loader has to find. The object file is also half the size, and it has no .eh_frame entries for the frames it cannot unwind.

And note what the object file does not need: malloc, __cxa_allocate_exception, _Unwind_Resume, __gxx_personality_v0, or any RTTI. The eleven-line dependency of 20.9 is genuinely gone, not hidden.

3. The Ban Flag Does Not Touch Them

The first thing people ask is whether the build flags from 19.7 break herbceptions. They do not, because herbceptions are not exceptions:

$ g++ -std=c++26 -fherbceptions -fno-exceptions -O2 hb.cc -o hb -lherbceptions
  [exit 0]

-fno-exceptions removes the legacy mechanism, and the herbception mechanism keeps working, because it never used it. A freestanding build can and should have -fno-exceptions -fno-rtti in it while using throws everywhere, which is exactly the combination the rest of this book uses.

4. The One Thing That Is Different: No Message Tables

The remaining dependency is the domain, and its job is to turn a code into text. Text is the one thing that has to be paid for in flash: a table of error strings per domain, plus the formatting code that copies them out. On a board where the entire binary is 64 KiB, that is a real cost for a string nobody will ever read.

So the runtime has a build knob. LIBHERBCEPTIONS_NO_MESSAGES (passed to the translation unit as __HERBCEPTIONS_NO_MESSAGES; the freestanding build option LIBHERBCEPTIONS_FREESTANDING defaults it on) makes every domain report only [domain](code). Here is the same program, once with the hosted runtime and once with a runtime built with that macro, printing name, message, name+message and the error itself:

--- hosted runtime ---
name         posix
message      (12)Not enough space
name_message [posix](12)Not enough space
e            [posix](12)Not enough space

--- runtime built with __HERBCEPTIONS_NO_MESSAGES ---
name         posix
message      (12)
name_message [posix](12)
e            [posix](12)

Same code, same error, same domain. What disappeared is the text. And it is not a stub that formats an empty string: the message table is never touched, which is where the savings come from. Built from the same source, the two runtimes:

$ size -A libhb_msg.so | grep -E '^\.(text|rodata)|^Total'
.text                 8771    4352
.rodata               3427   16384
Total                17964
$ size -A libhb_nomsg.so | grep -E '^\.(text|rodata)|^Total'
.text                 5587    4288
.rodata                291   12288
Total                11164

6,800 bytes of a 17,964-byte runtime, and 91% of its read-only data, for one domain — the POSIX one. Add the parse, HTTP, cmath and Win32 domains and the combined table is the largest thing in the library. On x86-64 those numbers are noise; on the targets this chapter is about they decide whether the feature fits.

5. And You Can Put Your Own Text Back

"No messages" is not a downgrade you have to live with forever, because a domain is overridable (19.1). A kernel that wants descriptive text for the twenty codes it actually reports can replace the POSIX domain with its own implementation, compiled into its own binary, with its own strings and its own do_query_information. The library gives you a default that costs nothing; you decide whether to pay for words.

Legacy exceptions (20.9)Herbceptions
Linker symbols needed111, a data pointer
Heap at failure timeYesNo
Stack walkYesNo
Frame tables, personality routineYesNo
RTTIYesNo
Works on a board, in a kernel, on a GPUNoYes
Message textAllocated at throw timeA build option; overridable per domain

That table is the whole reason this book teaches throws instead of throw. The error handling is the same feature on both rows; only the implementation changed, and the implementation was the problem.