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 标准近期关于“平凡无限循环”(trivial infinite loops)处理方式的变更。 此前,无限循环(如 `while(true);`)被视为“未定义行为”(UB),这使得编译器在优化时可以将其删除或以出乎意料的方式修改。C++26 的更新通过强制执行“前进进度语义”(forward-progress semantics)有效终结了这一做法,通常表现为隐式调用 `std::this_thread::yield()`。 社区对此反应不一,且批评声强烈: * **“隐藏”逻辑**:许多开发者不满于简单的无限循环现在会自动触发系统调用。这在资源受限的嵌入式或内核环境中尤为危险,因为这些环境中可能不存在此类调用,或者会导致意外的副作用。 * **性能与可预测性**:批评者认为,“前进进度”这一理由只是为了进行激进优化的借口,这种优化将理论上的基准测试收益置于“所见即所得”的代码可预测性之上。 * **嵌入式担忧**:对于将无限循环作为标准“错误停机”惯用语的嵌入式开发者来说,他们认为委员会为了追求高层抽象而忽视了系统编程的需求,这引发了对代码退化的担忧。 总体而言,这场讨论反映出开发者对 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