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

C++26 改变了语言对 `while (true);` 之类“平凡”无限循环的处理方式;这类循环此前属于未定义行为,可能被编译器以令人惊讶的方式优化掉,甚至让控制流落入无关代码。评论者争论这一长期规则是否真的有意义,指出在嵌入式和内核代码中,这类循环常用于出错后停机或等待中断。新标准把这些循环建模为调用 `std::this_thread::yield()`,一些人认为这是一种满足“向前进展”要求的务实做法,另一些人则认为这会在一个本就受未定义行为困扰的系统编程语言里引入不必要的隐藏系统调用和复杂性。

变更范围与令人惊讶的行为

  • 许多评论者对“平凡的无限循环在将近六分之一世纪里都属于 UB”感到震惊,因为这会导致诸如以下情况:
    • 当编译器移除循环和尾声代码时,函数会“掉落”到后续函数中。
    • 没有返回语句的非 void 函数会继续执行到其他代码中。
  • 一些人认为,这严重违背了直觉中的调试预期(如果“删代码直到问题消失”都不管用,那么一个最小循环就可能做任何事)。

为什么无限循环曾经是 UB(优化 / 向前进展)

  • 多篇帖子将此与“向前进展保证”联系起来:编译器可以假设线程最终会取得进展(返回、I/O、volatile、原子操作)。
  • 对无限循环设为 UB 使得以下优化成为可能:
    • 在不证明终止性的情况下进行循环合并或重排。
    • 诸如将 store 提前到纯计算之前之类的变换。
  • 一些人并不相信这些优化值得付出语义上的怪异代价,认为这更多是基准测试竞赛,而不是真正能给用户带来好处。

嵌入式与系统场景

  • 对“从来没有理由使用平凡无限循环”这一说法,存在强烈分歧。
  • 嵌入式和裸机开发者指出:
    • 出错时停机的处理器。
    • 中断驱动系统中的主“超级循环”。
    • 通过保持清醒来避免昂贵的睡眠/唤醒切换。
  • 另一些人则认为,你应该使用专门的 CPU halt/sleep 指令,或使用能指示错误的循环(例如闪灯代码),而不是空转。

新的 C++26 行为(std::this_thread::yield

  • 新规则只定义了一个非常狭窄的情况:一个带常量条件、平凡空体的无限循环。
  • 批评主要集中在:
    • 用隐式的 this_thread::yield() 调用来替代它,也就是在看起来没有副作用的代码中插入一个不可见的系统调用。
    • 在无宿主(freestanding)/ 嵌入式 / 沙箱环境或实时场景中的风险。
    • 不一致性:while(true);while(true) continue; 以及其他写法会有不同语义。
  • 一些人更愿意接受这比 UB 好,但仍称其“糟糕透顶”,并宁愿看到:
    • 一个诊断/警告。
    • 一个标准库里的“停在这里”原语。
    • 或者采用 C11 风格的规则(带常量条件的无限循环就简单地定义为不终止)。

更广泛的 UB 与 C++ 发展方向担忧

  • 关于 UB 作为优化工具,还是作为误编译和难以理解行为来源的长篇讨论。
  • 与 C 和 Rust 的比较:
    • C 对常量表达式循环的特殊处理被认为更合理。
    • 提到 Rust 的 loop {} 以及 LLVM 的历史,作为此前推动修复的压力。
  • 多位评论者感叹现代 C++ 中隐藏行为和复杂性不断增加,并担心无宿主 / 嵌入式用途会变得越来越难。