C++26 ends UB in trivial infinite loops
The C++26 standard makes loops like while(true); well-defined, previously classified as undefined behavior, a problem that has already caused error-handling code to be removed in optimized builds.

The C++26 standard makes loops like while(true); well-defined, previously classified as undefined behavior, a problem that has already caused error-handling code to be removed in optimized builds.
Up until C++23, a snippet as simple as while (true); was, technically, undefined behavior (UB). C++26 closes this gap through the P2809R3 proposal, as developer Sandor Dargo detailed on his blog. The change seems cosmetic at first glance, but it fixes a real problem that has already brought down error-handling logic in embedded systems.
The bug that wasn't a bug
Dargo's article reproduces an example on Compiler Explorer that shows the practical damage: a main() with an empty while (true); followed by an unreachable() function that prints "Hello world!". In theory, this function should never be called, since the loop never ends. But when compiled with Clang, the program prints the message.
// https://godbolt.org/z/WYMxxeW1T
#include <iostream>
int main() {
while (true)
;
}
void unreachable() {
std::cout << "Hello world!" << std::endl;
}The reason is the forward progress guarantee rule, introduced in C++11 along with thread support. The standard ([intro.progress]) allows the implementation to assume that any thread will eventually terminate, call a library I/O function, access a volatile glvalue, or perform a synchronization or atomic operation. An empty while (true); does none of that. So, under pre-C++26 rules, an execution stuck in that loop forever has undefined behavior, and the optimizer is free to assume the execution never gets stuck there, removing the loop and treating the following code as reachable.
Dargo points out that this is not a compiler bug: it is exactly what the standard allowed. "This is not a compiler bug, it's just UB, still better than nasal demons," he writes, referencing the classic C/C++ community expression describing the extreme consequences of UB.
Why anyone would write this on purpose
The central point of the article is that while (true); is not accidental, bad, or rare code. It is a common pattern in firmware, drivers, and kernel code to handle a fatal error when there is no operating system to return to:
if (hardware_init_failed()) {
log_error("fatal: hardware init failed");
while (true)
; // halt - there's nothing left to do
}The problem, according to Dargo, is that this fatal-error handler, written to simply halt the device, also fell under the same UB. An aggressive optimizer could remove the loop and let execution fall through to whatever code the linker placed right after, exactly as in the "Hello world!" example. In a real embedded system, this means the fatal-error handler does not halt the hardware: the device keeps running in a corrupted state, executing whatever instruction comes next. In safety-critical code, this is a concrete vulnerability, not an academic curiosity.
The historical divergence with C
A detail the article highlights is that C had already solved this correctly since C11. C++11 and C11 introduced forward progress rules around the same time, but C included one extra rule: loops whose controlling expression is a constant expression cannot be assumed to terminate. That's why while (1); was always well-defined in C. C++ never adopted this extra rule, creating an unnecessary divergence between the two languages: the same code, while (1);, was safe in C and UB in C++.
What exactly C++26 defines
P2809R3 does not copy C's rule broadly, which could inhibit legitimate optimizations in other kinds of loops. Instead, the proposal creates a narrow category called trivial infinite loop, defined by two simultaneous conditions:
- The loop must be a trivially empty iteration statement: the body must be literally empty (
;or{}). Any statement in the body, even an expression statement with no effect like"a string";, disqualifies the loop. - The controlling expression must be a constant expression that evaluates to
true. In aforwith no condition,trueis implicit.
The table in the original article shows concrete examples of what qualifies:
| Code | Trivial infinite loop? | |---|---| | while (true); | Yes | | for (;;); | Yes | | do {} while (true); | Yes | | constexpr bool go = true; while (go); | Yes, go is a constant expression | | while (true) { "a string"; } | No, body has a statement | | while (true) if (done) break; | No, body is not empty | | bool done = false; while (!done); | No, not a constant expression |
When both conditions are met, the loop's body is semantically replaced by a call to std::this_thread::yield(). This gives the loop's execution the forward progress semantics that were missing before, and the optimizer can no longer treat that specific loop as UB.
The caveat for freestanding
For freestanding implementations, that is, without an underlying operating system, typical of firmware and bare metal, the replacement with std::this_thread::yield() is implementation-defined: the compiler may or may not apply it. This matters precisely for the use case that motivated the change: turning a deliberate halt loop into a cooperative yield could introduce behavior the embedded systems programmer never intended. The standard leaves this decision to each freestanding toolchain implementation.
What changes for those who program in C++ today
It's worth noting that P2809R3 was also accepted as a defect report, which means compilers can retroactively apply the fix to earlier language modes. In practice, this means that anyone trying to reproduce the old behavior (the impossible "Hello world!") on a recent compiler may not succeed, even when compiling in C++20 mode, because the fix may already be present in the compiler for that mode.
For those who maintain low-level code, the practical takeaway is: fatal-error handlers written as while (true); no longer depend on UB to work as a halt, which is good news for security reviews in firmware. As for those working with concurrency who expect an explicit yield to happen in busy-wait loops, it's worth checking the compiler documentation about behavior in freestanding builds before assuming the replacement with std::this_thread::yield() is actually happening.
Translated from the Brazilian Portuguese original · Read the original
Perplexity swaps DynamoDB for in-house database and cuts latency by 5x
The company behind the AI-powered search engine migrated its serving layer to CobbleDB, an internal database written in Rust, and cut batch read latency by up to 5x while saving at least 20% on storage.