C++26:简单的无限循环不再属于未定义行为
C++26: Trivial infinite loops are no longer undefined behaviour

原始链接: https://www.sandordargo.com/blog/2026/09/16/cpp26-trivial-infinite-loops

在 C++26 之前,空的 `while (true);` 循环被视为未定义行为(UB)。由于 C++ 标准要求线程必须表现出“前向进度”(forward progress),编译器被允许假设此类循环不可能存在,并将其彻底优化掉。这往往会导致程序逻辑“穿透”并执行后续代码,从而引发重大的安全和稳定性风险,特别是在将此类循环用作致命错误停机机制的嵌入式系统中。 C++26 通过 P2809R3 提案解决了这一问题,将这类循环归类为“平凡无限循环”(trivial infinite loops)。要符合该定义,循环必须具有计算结果为 `true` 的常量表达式且循环体完全为空。这些循环现已被正式定义为合法行为,编译器不得再将其优化掉。为确保与前向进度保证的兼容性,标准现在将平凡无限循环的持续执行视为一种有效的终止状态。值得注意的是,在独立式(裸机)实现中,其确切行为仍由具体实现决定,以避免引入如让出执行权(yielding)等不必要的副作用,从而确保关键的“错误停机”模式能按预期工作。

在 C++26 标准中,针对“平凡”无限循环(如 `while(true);`)的未定义行为(UB)进行了修订。此前,编译器可以将此类循环视为未定义行为并对其进行优化(即直接移除),这在系统编程(如错误停机模式或裸机开发)中引发了诸多问题。 此次修复通过将循环体替换为对 `std::this_thread::yield()` 的调用来解决此问题。这一变更确保了“向前推进”语义,防止循环阻塞那些要求线程必须让出执行权的运行环境。 Hacker News 社区对此反响不一,且批评声音占据主流: * **惊讶与控制权:** 许多开发者认为,一个简单的循环在编译后被隐式注入系统调用是不可接受的。在受限环境(如内核、嵌入式系统或沙盒)中,这种做法可能并不适用,甚至根本无法实现。 * **语言哲学:** 批评者认为,C++ 越来越倾向于以“编译器自动注入的代码”来取代程序员的明确意图。他们建议,如果无限循环存在问题,应通过编译器警告或显式的让出请求来处理,而非通过隐式的行为变更。 * **复杂性:** 另一些观点指出,“未定义行为”已成为编译器优化的万能借口,这不仅增加了调试难度,还削弱了系统级开发所必需的可预测性。
相关文章

原文

Let’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 changes

C++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:

  1. 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.

  2. 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);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 contains a statement
while (true) if (done) break;No — body is not empty
while (true) if constexpr (false) break;No — doesn’t match the syntax of a trivially empty iteration statement
bool done = false; while (!done);No — not a constant expression

The 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 caveat

On 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.

Conclusion

while (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 deeper

If you liked this article, please

联系我们 contact @ memedata.com