C++ 生命周期结束指针清零与 OOTA 进展
摘要
在2026年6月于布尔诺举行的C++标准委员会会议上,三篇关于启用生命周期结束指针清零的论文被投票纳入C++29,解决了自1998年标准以来存在的问题,并且在禁止标准草案中出现凭空产生(OOTA)值方面取得了进展。
<p><a href="https://lobste.rs/s/p6u8pf/c_lifetime_end_pointer_zap_oota_progress">评论</a></p>
查看缓存全文
缓存时间: 2026/06/24 13:58
# C++ 生命周期结束指针归零与 OOTA 进展
来源:https://people.kernel.org/paulmck/c-pointer-zap-and-oota-progress
在 2026 年 6 月初于布尔诺举行的 C++ 标准委员会会议上,生命周期结束指针归零 (lifetime-end pointer zap) 和无中生有 (out-of-thin-air, OOTA) 访问均取得了重大进展!
## 生命周期结束指针归零
生命周期结束指针归零在 2025 年 8 月的一篇博客文章(https://people.kernel.org/paulmck/what-on-earth-does-lifetime-end-pointer-zap-have-to-do-with-rcu)中有详细描述。该文章详细介绍了四份 C++ 工作论文,其中三份的后续版本现已投票纳入 C++29:
1. Davis Herring 的 P2434R4(“非确定性的指针来源”)(https://isocpp.org/files/papers/P2434R4.html)为其他三篇论文奠定了基础。
2. P2414R10(“指针生命周期结束归零的解决方案”)(https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p2414r10.pdf)定义了针对无效指针的原子操作和 volatile 操作。(实际上,被接受的是更新后的 P2414R12,但该版本在即将发布的布尔诺会议后邮件列表中才能方便获取。)
3. P3347R5(“无效/预期指针操作”)(https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3347r5.pdf)定义了无效指针的加载和存储操作。(实际上,被接受的是更新后的 P3347R6,但同样在即将发布的布尔诺会议后邮件列表中才能方便获取。)
这三篇论文足以使著名的 LIFO Push 算法能够直接用 C++29 编写。换句话说,这些论文正在解决一个可追溯到 1998 年第一个 C++ 标准的问题。如果我们能将这些改动引入 C 语言,那么 Linux 内核中 `include/linux/llist.h` 文件里易受归零影响的代码将得到追溯性认可,一直追溯到 1989 年第一个 C 标准。
第四篇论文 P3790R1(“指针生命周期结束归零的解决方案:比特袋指针类”)(https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3790r1.pdf)没有遭到根本性反对,但需要进行大规模的措辞修订。更新后的 P3790R2 将出现在即将发布的布尔诺会议后邮件列表中。如果顺利,它将在 2026 年 11 月的会议上被投票纳入 C++29 标准。
从我的 P3790R1 错误中汲取教训:尽早寻求措辞帮助!
## 无中生有 (OOTA) 访问
2026 年 5 月版的 C++ 标准草案 N5046(“工作草案,编程语言 — C++”)(https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/n5046.pdf)在第 32.5.4p8 [atomics.order] 节中指出:
> 实现应确保不会计算出“无中生有”的值,这些值循环依赖于其自身的计算。
该标准草案随后给出了一些应被禁止的 OOTA 循环示例,首先是经典示例:
``
// 线程 1:
r1 = y.load(memory_order::relaxed);
x.store(r1, memory_order::relaxed);
// 线程 2:
r2 = x.load(memory_order::relaxed);
y.store(r2, memory_order::relaxed);
``
然后是稍微更复杂的示例:
``
// 线程 1:
r1 = x.load(memory_order::relaxed);
if (r1 == 42) y.store(42, memory_order::relaxed);
// 线程 2:
r2 = y.load(memory_order::relaxed);
if (r2 == 42) x.store(42, memory_order::relaxed);
``
在这两种情况下,OOTA 结果 `r1==r2==42` 都应被禁止。这些都是很好的建议,但我们不得不对那些想知道具体该做什么或不该做的人抱有同情。尤其考虑到 P3692R4(“如何真正不费吹灰之力避免 OOTA”)(https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3692r4.pdf)中展示的更复杂示例。
幸运的是,对于(大多数)编译为汇编指令然后在真实硬件上执行的 C++ 实现来说,有一条出路。毕竟,执行指令和通信数据都需要时间,这意味着上面展示的 OOTA 循环无法闭合。至少,如果我们愿意接受迄今为止证明时间不会倒流的经验证据的话。
虽然这种直觉简单且有说服力,但各种编译器优化带来了非常大的挑战。P3692R4 的大部分内容都在处理这些挑战,包括说服读者仅靠语义依赖直觉是不够的挑战。此外,P3692R4 的主体部分包含 Alan Stern 提供的一些精心选择的定义和巧妙的非正式证明。Alan 的定义和证明使我们能够断言以下内容:
> [注 7 —— 如果一个实现将源程序翻译到物理机器的指令序列,并对待非 volatile 的原子访问如同 volatile 一样,则该实现不会为无未定义行为的程序产生无中生有的值。如果一个实现仅进行单线程分析以支持省略和合并对同一对象的非 volatile 原子访问、重排对不同对象的非 volatile 原子访问(在 as-if 规则允许时)的优化,并且除此之外将非 volatile 原子访问视为 volatile 访问,则该实现不会为无未定义行为的程序产生无中生有的值。不满足这些限制的实现仍可能存在无中生有的值。—— 注结束。] [注 8 —— 与抽象机器不同,物理机器存在指令执行和信息传输延迟。物理机器会谨慎管理任何推测执行,以避免在硬件层面产生无中生有的值。—— 注结束。]
因此,如果你的代码没有未定义行为并且仅使用 volatile 原子操作,那么传统 C++ 编译器产生的代码不可能产生 OOTA,句号,故事结束。
然而,如果你使用非 volatile 原子操作,编译器必须限制其优化,但限制程度并不过分。关于现有的生产级 C++ 编译器是否能为非 volatile 原子操作避免 OOTA,目前仍有争论,当前(不令人满意的)状态是:我们无法证明它们总能避免 OOTA,但另一方面,我们也没有具体的 OOTA 例子。
尽管如此,P3692R4 确实为传统编译器(即使是针对非 volatile 原子操作)提供了一种避免 OOTA 的简单方法。这是 OOTA 问题四分之一世纪历史中的一个重要里程碑,因此该论文被投票纳入 C++29。
尽管这个里程碑很重要,但这并不是 OOTA 故事的终点。C 语言和 Rust 语言的实现者也可能会从类似的保证中受益,其中 C 语言的仅 volatile 原子操作使问题相对容易解决。
此外,P3692R4 对非时间性的形式化验证工具没有帮助,例如 herd7(https://github.com/herd/herdtools7)和 CPPMEM(http://svr-pes20-cppmem.cl.cam.ac.uk/cppmem/)。因为这些工具不模拟时间,所以无法利用时间流逝来排除 OOTA 循环。此外,P3692R4 对目标代码的依赖意味着它转而依赖于编译器使用有效的优化,而“有效优化”的定义目前是一个逐例进行的社会过程。幸运的是,Mark Batty 及其学生和合作者正在研究这个问题,如 P3692R4 中的参考文献 [34] 和 [35] 所示。
但如果 Mark Batty 等人正在致力于 OOTA 问题的完全数学解决方案,为什么还要费心采用 P3692R4 更有限的方法呢?
因为:(1) P3692R4 减轻了他们适配生产级编译器 CPU 和内存预算的需求;(2) P3692R4 现在就能用,并且解决了最紧迫的需求,从而让他们可以慢慢来,把事情做好。我个人希望他们目前的工作已经做对了,但根据我的经验,墨菲先生(https://en.wikipedia.org/wiki/Murphy%27s_law)总是有最终决定权。
## 致谢
非常感谢 Simon Cooksey、Mark Batty 和 Alan Stern 对本文早期版本提供的极好反馈!
相似文章
C++ 标准库在过去十五年间一直在自我撤步,证据公开
一份详细的目录,列出了从 C++11 到 C++26 期间被正式弃用、非正式不推荐或由于 ABI 约束实际上已损坏但无法修复的 C++ 标准库特性。文章指出,C++ 委员会推出一系列替代品来替换其自身特性的模式始终如一,其中包含一个基准测试,显示 Rust 和 C++ 标准库容器之间的 P99 延迟差异高达 58 倍。
C++26:标准库强化
C++26 引入了标准化的库强化机制,用于在运行时捕获常见的未定义行为(如越界访问)。基于 Google 的生产经验,此举仅带来 0.30% 的性能开销,同时将段错误减少了 30%。
安全 Rust 的边界
TokioConf 2026 的一篇演讲/博客文章探讨了如何通过为复杂指针结构实现追踪式垃圾回收,将安全 Rust 推向极限,并分享处理循环引用与原始指针 GC 设计的技巧。
C语言中在C++中仍然无法工作的构造——以及一些已发生变化的构造
一篇更新经典调查的博文,关于C语言中在C++中无法工作的构造,涵盖了C++20和C23标准中影响兼容性的变化。
正统 C++ (2016)
正统C++是C++的一个最小子集,避免使用现代特性,倡导更简单、类似C的风格,以提高可读性和兼容性。