Ch19.7: Ban Legacy Exceptions With Your Build Flags

中文(简体)

1. The Flags

Saying "we do not use legacy exceptions" in a document changes nothing: in six months somebody will include a header that throws, the unwinder will come back into the link, and nobody will notice until a target that cannot unwind fails to build. Make it mechanical. Every translation unit you compile, always:


-fno-exceptions -fno-rtti -fno-unwind-tables -fno-asynchronous-unwind-tables -fno-ident

What each one removes:

Flag What it makes impossible
-fno-exceptions throw, try, catch anywhere in the build, and with them the whole __cxa_* runtime and the personality routine. Herbceptions still work: throws and catch throws are a different mechanism and are unaffected
-fno-rtti typeid, dynamic_cast, and the type-info objects that legacy handler matching is built from. Herbceptions do not use RTTI, so nothing on the throws channel is affected
-fno-unwind-tables The .eh_frame frame tables that a stack walker needs
-fno-asynchronous-unwind-tables The same tables in their asynchronous form, which debuggers and profilers also use
-fno-ident The compiler's identifying strings in .comment, which leak build machine details and are pure noise in a binary

These are not stylistic. Each one deletes a mechanism, and the build fails loudly the moment somebody tries to use it.

2. What It Actually Removes

One small program, one herbception, compiled twice: once with the default flags, once with the list above.


#include <fast_io.h>

int divide(int a, int b) throws
{
	if (b == 0)
	{
		throw throws ::std::errc::invalid_argument;
	}
	return a / b;
}

int main()
{
	int result{};
	try
	{
		result = divide(84, 2);
	}
	catch throws (::std::error e)
	{
		::fast_io::io::print("error ", e, "\n");
		return 1;
	}
	::fast_io::io::print("84/2 = ", result, "\n");
}


84/2 = 42

Same output, smaller binary, and the difference is exactly the machinery of a feature the program never used:

With default flags With the ban list What it is
5,869 bytes of .text 4,859 bytes Code, including the personality-routine reachable parts
.eh_frame 644 bytes 128 bytes The frame tables the unwinder walks
.eh_frame_hdr 116 bytes 44 bytes The binary-search index into those tables
.gcc_except_table 16 bytes gone The language exception table: ranges, landing pads, handler types
13 FDEs 4 FDEs How many functions carry unwind information at all
.comment 111 bytes 69 bytes Compiler version strings

The four remaining FDEs come from the C runtime startup objects, not from your code, so this is the floor. On a target without a loader there is nothing to register them with, and that is the point: the flags make your program's requirements match the platforms it has to run on.

3. The Diagnostic You Want

With the ban list in place, a legacy exception is a compile error at the line that introduces it, not a mysterious runtime failure on a target that cannot unwind:


legacy_demo.cc: In function 'int main()':
legacy_demo.cc:13:78: error: exception handling disabled, use '-fexceptions' to enable
   13 |                                 throw ::std::runtime_error("division by zero");
      |                                                                              ^

4. Put It in the Build, Not in a Wiki

With a plain makefile:


CXXFLAGS += -std=c++26 -fherbceptions \
            -fno-exceptions -fno-rtti -fno-unwind-tables \
            -fno-asynchronous-unwind-tables -fno-ident
LDFLAGS  += -lherbceptions

With CMake, applied to every target in the project and not only to the executable:


add_compile_options(
  -fherbceptions
  -fno-exceptions -fno-rtti
  -fno-unwind-tables -fno-asynchronous-unwind-tables
  -fno-ident)

add_compile_options is deliberately global: a static library compiled without the flags can reintroduce the behaviour into an executable compiled with them, and the failure then shows up at link time in a target that looks unrelated. One list, every target, including tests and benchmarks.

5. What You Give Up, and Why It Is Not a Loss

If you take one line from this chapter, take the flag list. A project that compiles with it cannot drift back into legacy exceptions, and every target that has no unwinder, no loader and no C++ runtime stays a target you can build for.