LmCast :: Stay tuned in

C++26: Trivial infinite loops are no longer undefined behaviour

Recorded: Sept. 18, 2026, 3:08 p.m.

Original Summarized

C++26: Trivial infinite loops are no longer undefined behaviour | Sandor Dargo's Blog Sandor Dargo's BlogOn C++, software development and books HOME TAGS ARCHIVES BOOKS SPEAKING DAILY C++ WORKSHOPS HI... SUBSCRIBE Blog 2026 09 16 C++26: Trivial infinite loops are no longer undefined behaviour Post CancelC++26: Trivial infinite loops are no longer undefined behaviour Sandor Dargo Sep 16 2026-09-16T00:00:00+02:00 5 minLet’s start with a question! Is this program well-defined?1
2
3
4
int main() {
while (true)
;
}
If you said yes, you’d be wrong — at least before C++26. A while (true); loop with no side effects used to be undefined behaviour. Compilers were free to assume it terminates, and some — Clang in particular — would optimize it away entirely, with spectacular consequences:1
2
3
4
5
6
7
8
9
10
11
// https://godbolt.org/z/WYMxxeW1T
#include <iostream>

int main() {
while (true)
;
}

void unreachable() {
std::cout << "Hello world!" << std::endl;
}
In Clang, this prints “Hello world!”. The compiler removes the infinite loop, main falls through, and the linker-placed unreachable() function executes. This is not a compiler bug — it’s just UB, still better than nasal demons.Recently, I wrote about how C++26 reduces undefined behaviour, covering changes like erroneous behaviour for uninitialized reads and making incomplete-type deletes ill-formed. I completely forgot about this one. I only realized while preparing for an upcoming CppCon talk on C++26 features — so here it is now.C++26 fixes this with P2809R3. Trivial infinite loops are now well-defined. The mentioned proposal was also accepted as a defect report, so implementations may apply the fix to earlier C++ modes as well. That is why you might not be able to reproduce the old behaviour on a recent compiler even in C++20 mode.How did we get here?The story starts with the forward progress guarantee, introduced in C++11 alongside threading support. The standard says ([intro.progress]) that the implementation may assume any thread will eventually do one of the following: terminate, call a library I/O function, access a volatile glvalue, or perform a synchronization or atomic operation.A while (true); loop does none of those things. Under the pre-C++26 forward-progress rules, an execution that remains in such a loop forever has undefined behaviour. The optimizer can therefore assume that execution never gets stuck there, which enables transformations that remove the loop and mark the path as unreachable.The funny bit is that C got this right. C++11 and C11 both introduced forward-progress rules, but C included one more rule: loops whose controlling expression is a constant expression may not be assumed to terminate. So while (1); is well-defined in C11 and ever since.C++ never adopted that extra rule. The result was the unnecessary divergence just described, but let’s repeat it: while (1); was well-defined in C but undefined behaviour in C++.But why would anyone write while (true); in the first place?What I found is that this is common in embedded and kernel code as a halt-on-error pattern. When a fatal error occurs and there’s no operating system to exit to, you simply stop:1
2
3
4
5
if (hardware_init_failed()) {
log_error("fatal: hardware init failed");
while (true)
; // halt — there's nothing left to do
}
This is not simply a common pattern on bare metal — it was also undefined behaviour in C++. The consequences aren’t theoretical. When the optimizer removes the loop, execution falls through into whatever code the linker placed after it — as the “Hello world!” example at the top of this article demonstrates. In an embedded system, that means a fatal error handler doesn’t actually halt the device. The hardware keeps running in a corrupt state, executing whatever instructions happen to follow. In security-critical code, that’s a real vulnerability.What C++26 changesC++26 doesn’t simply copy C’s rule, though. That approach was considered and rejected. C protects a much broader set of loops — broadly, loops whose controlling expression is a constant expression — which could inhibit useful optimizations. Instead, P2809R3 defines a deliberately narrow category: the trivial infinite loop. It’s defined by two conditions:The loop must be a trivially empty iteration statement — meaning its body is literally empty (; or {}). Any non-empty statement in the body, even a meaningless expression statement such as "a string";, disqualifies it.The controlling expression must be a constant expression that evaluates to true. For a for loop with no condition, true is implicit.When both conditions are met, the loop body is replaced with a call to std::this_thread::yield(). This gives execution of the loop the forward-progress semantics it previously lacked.Here’s what qualifies and what doesn’t:CodeTrivial infinite loop?while (true);Yesfor (;;);Yesdo {} while (true);Yesconstexpr bool go = true; while (go);Yes — go is a constant expressionwhile (true) { "a string"; }No — body contains a statementwhile (true) if (done) break;No — body is not emptywhile (true) if constexpr (false) break;No — doesn’t match the syntax of a trivially empty iteration statementbool done = false; while (!done);No — not a constant expressionThe change also updates the forward progress guarantee itself: a thread may now “continue execution of a trivial infinite loop” as one of the things it’s assumed to eventually do. The optimizer can therefore no longer treat a trivial infinite loop as undefined behaviour and assume that execution continues past it.The freestanding caveatOn freestanding implementations, it is implementation-defined whether the replacement with std::this_thread::yield() occurs at all. That’s important for bare-metal systems: turning a deliberate halt loop into a cooperative yield could introduce behaviour the programmer never intended.Conclusionwhile (true); being undefined behaviour was one of those C++ facts that surprised everyone who heard it. It was an unnecessary divergence from C, it broke real embedded code, and compilers genuinely exploited it. C++26 fixes it — trivial infinite loops are now well-defined, and the compiler can no longer optimize them away.Connect deeperIf you liked this article, pleasehit on the like button,subscribe to my newsletterand let's connect on Twitter!if you're preparing for a C++ quant/trading interview, check out GetCracked dev cpp cpp26 undefinedbehaviour This post is licensed under CC BY 4.0 by the author. Share Recent UpdateC++26: std::hiveC++26: Deprecating or removing library featuresC++26: std::polymorphicC++26: std::indirectC++26: Cleaning up string literals Trending Tags cpp books watercooler career tutorial cpp23 cpp26 stl algorithms self-improvement ContentsFurther Reading Feb 26, 2025 2025-02-26T00:00:00+01:00 C++26: no more UB in lexing and preprocessing If you ever used C++, for sure you had to face undefined behaviour. Even though it gives extra freedom for implementers, it’s dreaded by developers as it may cause havoc in your systems and it’s be... Jul 29 2026-07-29T00:00:00+02:00 C++26: Reducing undefined behaviour Nowadays, safety is in the focus of C++. Whether it’s at committee meetings, conference talks, or hallway discussions, the topic keeps coming up. C++26 made some big steps forward in this area, and... Dec 11, 2024 2024-12-11T00:00:00+01:00 Let's start exploring C++26 During the last 2 years, we spent a lot of time exploring C++23 resulting in almost 40 blog posts. I’m not saying that we covered every single new language or library feature, but we covered most o... C++26: Module improvements - Comments powered by Disqus. For C++ developers who give a damnBetter code. Better career. One email a week on modern C++ and what it takes to grow — written by someone still figuring it out too.​SubscribeWe won't send you spam. Unsubscribe at any time.Built with Kit © 2026 Sandor Dargo. Some rights reserved. Powered by Jekyll with Chirpy theme.Trending Tagscpp books watercooler career tutorial cpp23 cpp26 stl algorithms self improvement

The issue addressed by C++26 concerns the status of trivial infinite loops, which were previously classified as undefined behavior, leading to unpredictable compiler optimizations. Before C++26, a while (true); loop without side effects was considered undefined behavior because the compiler was free to assume it would never terminate. This assumption allowed optimizers, such as Clang, to remove the loop entirely, causing execution to fall through to subsequent code, which could result in executing logically unreachable functions, a phenomenon known as undefined behavior.

This divergence arose from the history of forward progress guarantees introduced in C++11. The standard stipulated that an execution must eventually perform an action that causes termination, calls a library I/O function, accesses volatile memory, or performs a synchronization or atomic operation. A loop that runs indefinitely does none of these things, meaning it did not guarantee forward progress under the previous rules. While C included a rule stating that loops controlling constant expressions may not be assumed to terminate, C++ omitted this rule, resulting in the observed undefined behavior.

The motivation behind this issue stems from usage patterns in systems programming, particularly in embedded and kernel code where infinite loops are sometimes used as a halt-on-error mechanism. The undefined behavior allowed compilers to remove these halt loops, which was critically dangerous in security-sensitive code, as it could lead to fatal error handlers failing to halt hardware, allowing the system to continue executing in an erroneous state and creating vulnerabilities.

C++26 remedies this by introducing a specific rule, Proposal P2809R3, to define and constrain these loops. Trivial infinite loops are now well-defined, provided they meet two strict conditions: the loop body must be a trivially empty iteration statement, meaning it contains no executable statements like semicolons or braces, and the controlling expression must be a constant expression that evaluates to true. When these conditions are met, the language mandates that the loop body is replaced with a call to the std::this_thread::yield() function, thereby granting the loop the forward-progress semantics that were previously absent.

This update modifies the forward progress guarantee itself, allowing a thread to be assumed to "continue execution of a trivial infinite loop." Consequently, compilers can no longer treat these specific loops as undefined behavior and eliminate them via optimization. Although this change provides significant safety in general application development, implementations must remain mindful of the freestanding caveat: for freestanding environments, it is implementation-defined whether the replacement with std::this_thread::yield() actually occurs, which has significant implications for specialized bare-metal systems.