Here is a fact that surprises almost everyone who hears it: in C++, while (true); — an empty loop with a constant-true condition — was undefined behaviour. Not since some ancient standard, but from C++11 onward. Sandor Dargo walks through the standardese, the compiler behaviour it licensed, and the C++26 fix.
The practical consequence is not theoretical. Clang would remove the loop and let execution fall through into whatever the linker placed next, which is how the canonical Godbolt example ends up printing “Hello world!” from a function that was never called. That is not a compiler bug. It is a correct optimisation applied to code the standard said had no defined meaning.
Why it bit real code:
- The halt-on-error pattern —
while (true);after a fatal hardware init failure, when there is no operating system to exit to — is normal on bare metal. - An empty loop does none of the things the C++11 forward-progress guarantee assumes a thread will eventually do (terminate, do I/O, touch a
volatile, synchronise), so remaining in it forever was undefined. - With the loop optimised away, a fatal error handler does not halt the device. The hardware keeps running in a corrupt state, executing whatever follows. On security-critical code, that is a vulnerability.
The part worth remembering is that C got this right in 2011: C added a carve-out saying loops with a constant controlling expression may not be assumed to terminate, and C++ never copied it. So while (1); was well-defined in C and undefined in C++ for fifteen years.
C++26 fixes it with P2809R3, but not by copying C — that was considered and rejected as too broad, since it would inhibit useful optimisations. Instead the paper defines a deliberately narrow category, the trivial infinite loop:
- The body must be literally empty —
;or{}. Even a meaningless statement like"a string";disqualifies it. - The controlling expression must be a constant expression evaluating to true, including the implicit condition of
for (;;). - When both hold, the body is treated as a call to
std::this_thread::yield(), which gives the loop the forward-progress semantics it was missing.
Two caveats are worth carrying forward. On freestanding implementations it is implementation-defined whether that yield() substitution happens at all — which matters on bare metal, where turning a deliberate halt into a cooperative yield is behaviour the programmer never asked for. And because the paper was also accepted as a defect report, compilers may apply the fix in earlier -std= modes, so the old behaviour is no longer reliably reproducible across toolchains.
The takeaway is less “C++ trivia” than a clean example of what undefined behaviour costs. The optimiser is not being hostile; it is exercising a permission the standard granted, and the code that paid for it was the code with no OS to fall back on. It is also a decent lesson in standards work: the fix that shipped was the narrowest one that covers the real idiom, plus an escape hatch for the environments that need the old semantics.