C++26: 平凡的无限循环不再是无定义行为

Lobsters Hottest 新闻

摘要

C++26 将平凡的无限循环标准化为定义明确的行为,消除了之前允许编译器优化掉这些循环的无定义行为,这在嵌入式和内核应用中是有问题的。

<p><a href="https://lobste.rs/s/uvrata/c_26_trivial_infinite_loops_are_no_longer">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/09/16 23:31

# C++26:平凡无限循环不再是未定义行为 来源:https://www.sandordargo.com/blog/2026/09/16/cpp26-trivial-infinite-loops 让我们先提一个问题!以下程序是否定义明确? ```cpp int main() { while (true) ; } ``` 如果你回答“是”,那就错了——至少在C++26标准之前。一个没有副作用的 `while (true);` 循环过去属于*未定义行为*。编译器可以自由地假设它会终止,而某些编译器——尤其是Clang——会将其彻底优化掉,导致惊人的后果(https://godbolt.org/z/WYMxxeW1T): ```cpp // https://godbolt.org/z/WYMxxeW1T #include <iostream> int main() { while (true) ; } void unreachable() { std::cout << "Hello world!" << std::endl; } ``` 在Clang下,这会打印出“Hello world!”(https://godbolt.org/z/WYMxxeW1T)。编译器移除了无限循环,`main`函数直接落入后续代码,链接器放置的`unreachable()`函数得以执行。这并非编译器错误——这只是未定义行为,尽管它仍比“鼻子恶魔”(nasal demons)要好。 最近,我曾撰文讨论C++26如何减少未定义行为(https://www.sandordargo.com/blog/2026/07/29/cpp26-reduces-undefined-behaviour),涵盖了诸如将未初始化读取视为错误行为、使不完整类型删除无效等变化。但我完全忘记了这一项。直到为即将到来的C++26特性CppCon演讲(https://cppcon2026.sched.com/event/2RT3x/the-real-story-of-c++26-beyond-the-headline-features)做准备时才意识到——因此现在将其补充在此。 C++26通过P2809R3(https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p2809r3.html)修复了此问题。平凡无限循环现在定义明确。该提案也作为缺陷报告被接受,因此实现方可能会将此修复应用于更早的C++模式。这就是为什么即使在C++20模式下,你也可能无法在较新的编译器上复现旧行为。 ## 问题是如何产生的? 故事始于C++11引入的“前向进展保证”。标准规定([intro.progress]):*实现方可假设任何线程最终会执行以下操作之一:终止、调用库I/O函数、访问volatile左值,或执行同步/原子操作*。而 `while (true);` 循环未执行任何这些操作。在C++26之前的前向进展规则下,永远停留在此类循环中的执行流具有未定义行为。因此优化器可假设执行永远不会卡在此处,这使得移除循环并标记路径为不可达的优化成为可能。 有趣的是,C语言正确处理了这个问题。C++11和C11都引入了前向进展规则,但C语言额外包含一条规则:控制表达式为*常量表达式*的循环**不能**被假定为会终止。因此在C11及之后的版本中,`while (1);` 是定义明确的。C++从未采纳这条额外规则。结果就是上述不必要的差异:`while (1);` 在C中是定义明确的,而在C++中是未定义行为。 但为什么有人一开始会编写 `while (true);`?我发现这在嵌入式和内核代码中常作为*错误停机模式*使用。当发生致命错误且没有操作系统可供退出时,你只需停止执行: ```cpp if (hardware_init_failed()) { log_error("fatal: hardware init failed"); while (true) ; // 停机 — 已无事可做 } ``` 这不仅仅是裸机系统中的常见模式——在C++中它也曾是未定义行为。其后果并非理论性的。当优化器移除循环时,执行流会落入链接器在其后放置的任何代码中——正如本文开头的“Hello world!”示例所示。在嵌入式系统中,这意味着致命错误处理程序实际上并未停机。硬件将在损坏状态下继续运行,执行随后碰到的任何指令。在安全关键代码中,这会成为真正的漏洞。 ## C++26的改变 不过,C++26并未简单复制C的规则。该方案曾被考虑但被否决。C语言保护的循环范围更广——广义上包括控制表达式为常量表达式的循环——但这可能抑制有益的优化。因此,P2809R3定义了一个刻意狭窄的类别:**平凡无限循环**。其定义基于两个条件: 1. 循环必须是**平凡空迭代语句**——意味着其主体字面上为空(`;`或`{}`)。主体中的任何非空语句,甚至是无意义的表达式语句如 `"a string";`,都会使其不符合条件。 2. 控制表达式必须是一个求值结果为`true`的**常量表达式**。对于没有条件的 `for` 循环,`true`是隐含的。 当两个条件同时满足时,循环体将被替换为对 `std::this_thread::yield()` 的调用。这赋予了循环执行原本缺乏的前向进展语义。 以下示例展示了哪些符合条件,哪些不符合: | 代码 | 是否平凡无限循环? | |------|-------------------| | `while (true);` | 是 | | `for (;;);` | 是 | | `do {} while (true);` | 是 | | `constexpr bool go = true; while (go);` | 是——`go`是常量表达式 | | `while (true) { "a string"; }` | 否——主体包含语句 | | `while (true) if (done) break;` | 否——主体非空 | | `while (true) if constexpr (false) break;` | 否——不符合平凡空迭代语句的语法 | | `bool done = false; while (!done);` | 否——非常量表达式 | 此次更改也更新了前向进展保证本身:线程现在可被假设最终会“继续执行平凡无限循环”。因此优化器不再能将平凡无限循环视为未定义行为并假设执行会越过它继续。 ## 自由环境实现的注意事项 在自由环境实现中,是否替换为 `std::this_thread::yield()` 由实现方定义。这对裸机系统很重要:将有意的停机循环转换为协作式让步可能引入程序员从未预期的行为。 ## 结论 `while (true);` 是未定义行为这一事实,曾是C++中令所有听闻者惊讶的特性之一。它是与C语言不必要的差异,破坏了真实的嵌入式代码,并且编译器确实会利用这一点。C++26修复了此问题——平凡无限循环现在定义明确,编译器不再能将其优化掉。 ## 深入交流 如果你喜欢这篇文章,请: - 点击点赞按钮, - 订阅我的通讯(https://sandor-dargo.kit.com/e19f29b0a1) - 在Twitter上与我联系(https://twitter.com/SandorDargo)! - 如果你正在准备C++量化/交易面试,请查看GetCracked(https://www.getcracked.io/?via=sandor)

相似文章

C++26:减少未定义行为

Lobsters Hottest

C++26 引入了减少未定义行为的更改,特别是使删除指向不完整类型的指针成为格式错误,从而提高了程序安全性。

C++26:标准库强化

Lobsters Hottest

C++26 引入了标准化的库强化机制,用于在运行时捕获常见的未定义行为(如越界访问)。基于 Google 的生产经验,此举仅带来 0.30% 的性能开销,同时将段错误减少了 30%。

C语言中的一切皆为未定义行为

Hacker News Top

一位经验丰富的C++开发者认为,所有非平凡的C和C++代码都包含未定义行为,使得内存安全无法实现,并质疑这些语言在现代软件开发中的持续使用。

使用GCC的嵌套函数与宽指针及无蹦床 II

Lobsters Hottest

GCC 16 确保不捕获变量的嵌套函数无需蹦床,提升安全性。GCC 17 引入了内建函数,通过宽指针和静态链寄存器,即使对于捕获变量的嵌套函数也能避免蹦床。

减少 C 语言中的未定义行为

Hacker News Top

Martin Uecker 在 Kernel Recipes 2026 上发表了关于 C 语言中未定义行为的演讲,探讨了其产生的原因、抽象机器模型如何为其提供依据,以及 C 语言最终能否变得内存安全。