← 文章 / 编程开发
Hacker News 3小时前 · 2026-09-19 04:20:10 · 3 阅读

C++26:简单的无限循环不再是未定义行为

先来个问题!这段程序的行为是明确的吗?

1
2
3
4
int main() {
    while (true)
        ;
}

如果你回答“是”,那你就错了——至少在 C++26 之前如此。过去,一个没有任何副作用的 while (true); 循环属于未定义行为(UB)。编译器被允许假设该循环会终止,部分编译器(尤其是 Clang)甚至会将其彻底优化掉,后果相当惊人:点击此处查看

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;
}

在 Clang 中,这段代码会打印“Hello world!”。编译器移除了无限循环,main 函数直接执行完毕,随后链接器安置的 unreachable() 函数得以运行。这并非编译器 Bug,仅仅是 UB 的体现——虽然仍比“鼻恶魔”(Nasal Demons,指未定义行为可能导致的任意怪异结果)要好得多。

最近,我写过一篇关于C++26 如何减少未定义行为的文章,涵盖了诸如对未初始化变量读取视为错误行为、将删除不完整类型视为程序非法等变更。但我完全遗漏了这一条。直到为即将在 CppCon 上发表的关于 C++26 特性的演讲做准备时,我才意识到这一点——现在补上。

C++26 通过 P2809R3 提案解决了这个问题。如今,简单的无限循环是良定义的。该提案同时也被采纳为缺陷报告,因此实现方可以将此修复应用于更早的 C++ 标准模式。这就是为什么即使你在 C++20 模式下,也可能无法在较新的编译器上复现旧行为的原因。

事情是如何演变成这样的?

故事的起点是“前进保证”(forward progress guarantee),它随 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); 呢?

我发现这在嵌入式和内核代码中很常见,是一种出错即停止的模式。当发生致命错误且没有操作系统可供退出时,程序会直接停止运行:

1
2
3
4
5
if (hardware_init_failed()) {
    log_error("fatal: hardware init failed");
    while (true)
        ;  // halt — there's nothing left to do
}

这不仅仅是裸机开发中的常见模式,它在 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);否——不是常量表达式

这次修改也更新了向前推进保证本身:线程“继续执行平凡无限循环”现在被列为它最终会做的事情之一。因此,优化器不能再把平凡无限循环当作未定义行为,也就不能假设执行会越过它继续往下走。

自由实现环境的注意事项

在自由实现(freestanding)环境下,是否真的用 std::this_thread::yield() 做替换是由实现定义的。这对裸机系统很重要:把一个刻意的停机循环改成协作式 yield,可能引入程序员完全没有预期的行为。

结论

while (true); 曾经被认为是未定义行为,这是许多 C++ 开发者初学时都会感到惊讶的事实之一。它毫无必要地偏离了 C 语言,导致真实嵌入式代码出现问题,编译器也确实会利用这一特性进行优化。C++26 修正了这一点——现在简单的无限循环是明确定义的,编译器不能再将其优化掉。

连接更紧密

如果你喜欢这篇文章,请

原始来源: Hacker News

评论 (0)