Ch20.3: The C++03 Freestanding Lie

中文(简体)

1. "Freestanding" in the Language, Not in the Library

The previous page said C++03 required headers it could not implement. This page is about the worse half of the same problem: the language. In C++03, declaring your implementation freestanding changed the header list and nothing else. The grammar was identical. throw, try, catch, typeid, dynamic_cast, new and delete were all still there, still spelled the same way, still usable in any function.

If your target has no heap and no unwinder, that is not a smaller feature set — it is a language you cannot use as written. Three of its core constructs compile to calls into a runtime that does not exist on your board, and the compiler will happily emit them for you and let the linker complain. Here is what one throw costs at link time on a bare target:

void fail(int x) noexcept(false) { if (x < 0) { throw "negative"; } }

int main() { try { fail(-1); } catch (char const *) { return 0; } return 1; }
$ g++ -std=c++26 -O2 -nostdlib l2.cc -o l2
  undefined reference to `_Unwind_Resume
  undefined reference to `__cxa_allocate_exception
  undefined reference to `__cxa_begin_catch
  undefined reference to `__cxa_end_catch
  undefined reference to `__cxa_throw
  undefined reference to `__gxx_personality_v0
  undefined reference to `typeinfo for char const*

That is six runtime entry points and a type information object for a program whose only statement is a comparison. None of them can be written by you, and that is the point of this page. __cxa_allocate_exception needs a heap. _Unwind_Resume is the unwinder, which must understand your target's frame layout. __gxx_personality_v0 must be able to match a handler, which needs the RTTI hierarchy, which needs the vtable layout, which is the ABI. "Implementing legacy exceptions" means writing your own libsupc++, and the thing you would be writing is an operating system facility wearing a library's clothes.

2. The Forced Choice, and Why It Was Not a Choice

So a freestanding C++03 build had exactly one path: turn the features off and stop being conforming.

Feature the language requiresWhat it needs at run timeWhat a freestanding build did
throw / catch Heap for the exception object, unwinder, frame tables, personality routine, RTTI -fno-exceptions
typeid, dynamic_cast type_info objects, RTTI tables per class -fno-rtti
new / delete One global heap, ::operator new, and a failure policy No default heap, or a hand-written operator new
thread_local A thread pointer and TLS image setup -fno-threadsafe-statics, or nothing at all
Global constructors A .init_array runner in your start-up code Your own crt0, or static initialisation by hand

Every row means "the standard says this works; your build says it does not". A language whose freestanding mode requires you to disable its own core features is not a language with a freestanding mode. That is the sense in which C++03 freestanding was a lie — not that the committee lied, but that the document described something no vendor could ship and no user could rely on.

3. The Smallest Example: std::addressof Did Not Exist

Here is a case where the language's own design left a hole that the library could not fill — and the hole is one function. In C++03, &x was overloadable: any class could define operator& and change what "the address of" means. There was no way to ask for the real address.

::std::addressof arrived later, and it cannot be written by hand: it needs compiler magic (__builtin_addressof) to bypass a user-defined operator&, and it is constexpr. So for years there was a feature of the language — taking the address of an object — that could be silently redefined, with no escape hatch anywhere in the library. C has had &x mean the address of x since 1972.

The full story, including the modern one where a user-defined operator& can still make &s == nullptr true, is in 17.8. The reason it belongs in this chapter is different: ::std::addressof was not required in freestanding before C++23. So a kernel written in C++ could have a class with a hostile operator&, no way to defeat it, and a standard that did not promise the escape hatch would be there at all. A standard feature that is not freestanding does not exist — and this one did not exist where it mattered most.

4. Thirty Years of "Use the Compiler Switches"

The result is that the practical freestanding C++ of the 1998–2023 era is a dialect defined by flags rather than by the standard: -fno-exceptions -fno-rtti -nostdlib -fno-threadsafe-statics -nostdinc++, plus a hand-written operator new if you wanted one at all, plus a linker script, plus start-up code. Every embedded codebase invented that list independently, and every one of them is, by the letter of the standard, a non-conforming C++ program.

Herbceptions change this, and that is why they are the hinge of the book: 20.10 shows the same throw/catch program requiring exactly one linker symbol, a data pointer, no unwinder and no heap. The feature that made C++ unusable freestanding was not "error handling"; it was that the only error handling the language had was implemented on top of the operating system.