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 requires | What it needs at run time | What 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.