首页 差两票:C++26 被否决,和一个 28 年没人按过的按钮
文章
取消

差两票:C++26 被否决,和一个 28 年没人按过的按钮

10 月 24 日,一条 LinkedIn 帖子

2026 年 10 月 23 日,ISO 的 DIS 投票窗口关闭。第二天,Robert Seacord——ISO C 委员会(SC22/WG14)的召集人——在 LinkedIn 上把结果贴了出来:C++26 的草案国际标准(DIS)没有通过。

这件事在 C++ 的历史上没有发生过。Seacord 自己的说法是,他不认为 SC22 下属任何一个语言标准的 DIS 投票失败过。WG21 Direction Group 的 Daveed Vandevoorde 补了一句更狠的:过去没有任何一版 C++ 草案在 DIS 阶段拿到过哪怕一张反对票。

严格一点说,ISO 系统里确实有过一次例外:2006 年的 ISO/IEC DIS 26926,C++/CLI 语言规范。但那是 ECMA 提案转投 ISO 的独立项目,跟 WG21 维护的 C++ 标准不是一回事,结局是项目直接中止。Fertig 特意把这件事翻出来说,正是为了说明 10 月 23 日这个结果有多反常。

从 1998 年第一版 C++ 标准算起,二十八年,这个环节一直是走流程。各国国家团体(National Body,简称 NB)收到文本,投赞成,偶尔弃权,附几条编辑性意见,然后标准按期发布。所有参与者都知道这只是形式。

10 月 23 日之后,它不再是形式了。

一、差两票

DIS 阶段要同时满足两条,缺一不可:

条件要求实际结果
投赞成票的 P 成员比例至少 2/3(20 票中需 14 票)12 票,60%未达标
全部投票体中的反对票占比不超过 25%(22 票中最多 5 票)9 票,41%未达标

两条都没满足。赞成票差两票,反对票超了四票。

投反对的九个国家是:丹麦、芬兰、伊朗、意大利、日本、卢森堡、罗马尼亚、西班牙、瑞典。每个都随票提交了意见。法国和爱尔兰弃权,但也交了意见,所以一共十一个国家在案。波兰和美国弃权,没有意见。

请盯住这张表的算术。只要 25% 的反对,就能否掉 66.7% 的赞成。 这不是设计缺陷,恰恰相反,这是 ISO 有意为之的保守——共识不只是”多数同意”,少数派必须被听见。这套机制运行了二十八年没出过事,不是因为它不锋利,而是因为没人拔过刀。

现在刀被拔了。而它之所以被拔,是因为有人在投票前发了一篇论文(P4238R1),明确请求各国代表投反对票——理由是 contracts 的设计本身,以及它在委员会里的推进方式。换句话说,这不是九个国家各自独立的判断,而是一次有组织的、走完了全部合法程序的否决。

他们要拦下的东西,叫 contracts。

二、争的不是”要不要契约”,是”默认值归谁”

Contracts 是”把前置条件和后置条件写进函数签名”的机制。设计式契约(Design by Contract)这个概念从 Eiffel 时代就有了,C++ 社区要了它二十年。写出来是这样:

1
2
3
4
5
6
7
8
9
double sqrt(double x)
    pre(x >= 0)
    post(r: r >= 0);

void use(int* p)
    pre(p != nullptr)
{
    contract_assert(p != nullptr);
}

听上去没有任何争议。问题在于:这条 pre(x >= 0) 到底是一条编译器可以丢掉的检查,还是程序可以依赖的保证?

C++26 草案定义了四种求值语义:ignore(不求值)、observe(求值但不中止)、enforce(求值并中止)、quick-enforce(求值并快速中止)。四种都合法,而默认选哪一种,草案交给了实现。GCC 16 通过一个编译开关来暴露这个选择。

这就是整场争论的爆点。因为”标准”这个词的全部意义,本来是消除”这件事取决于实现”。C++ 之所以能跨编译器交付,靠的就是标准规定行为、实现照做。而 contracts 反过来把最关键的语义交回给了实现者——同一个头文件被两个翻译单元包含,一个开了 enforce,一个没开,两边对同一个函数的前置条件态度不同,链接在一起,程序没有任何一行违反规则,语义却已经分裂。

SG21 的前任主席 John Spicer 在论文 P4381R0(2026-09-19)里把这句话说得比我准:

“A check may be ignored, and different translation units may disagree, so a feature of that form is the wrong tool for a check that must run.”

一个可能被忽略、且不同翻译单元之间可能不一致的检查机制,不适合用来做”必须执行”的检查。

这句话往下推一步,就是库硬化的死局。C++ 的库硬化(library hardening)想干的事是:给 std::vector::operator[]、std::span 这类接口加上前置条件,越界就当场失败而不是默默 UB。这件事的前提是”检查始终开启”。而 P2900 的 contracts 前提是”检查默认可以关闭”。P3878 的结论因此很干脆:C++26 contracts 不是标准库硬化的可行基础。

于是在同一个特性上,两个目标互相拆台:要零开销,就得允许它消失;要可依赖,就不能允许它消失。 而草案选择了”两者都想要,具体看实现”。

更值得看的是 Spicer 这篇论文的标题:《Chairing SG21 and then objecting to Contracts》——我当过这个工作组的主席,然后我反对它的产物。他在文中承认了两处失败,其中一处是:

“I should have made those concerns clear to the whole of SG21 while the work was still in SG21. Waiting until the paper left the group was the wrong trade-off. It protected the appearance of a neutral chair at the cost of an honest technical process.”

他在等论文离开 SG21 之后才公开技术反对意见,理由是维持主席的中立外观——代价是一个诚实的技术流程。

这不是某个人反悔。这是一个信号:技术分歧被流程挤到了流程之外,而流程之外只剩一个国家体投票箱。

三、2019 年的对照组:那次他们选择卸车厢

C++20 也来过一次,剧本几乎一样。

契约的初版 P0542R5 在 2018 年拉珀斯维尔会议上进入 C++20 工作草案。随后一年多,反对论文一篇接一篇:未检查的契约被编译器当作”条件必然成立”,并据此激进地删掉分支代码——一旦前置条件在生产环境被打破,这个假设直接制造 UB。2019 年 2 月科纳,有人提出”要不要拔掉插头”(P1426R0),动议没过,因为大家觉得还有时间。到 7 月科隆,情况更糟了:设计还在会上临时改,原作者之间也不一致。

结局是 P1823R0,把 contracts 从 C++20 整段删除,一致通过(会议记录记为 68:0)。C++20 准点发车。

P1823R0 里有一句话,是”火车模型”的原始约定,我觉得它比任何技术论证都重要:

“Part of the train model must be an agreement that if there is not consensus that a feature is in acceptable form when the train is about to depart that the feature must be taken off the train and that it has to catch the next train.”

如果发车前没达成共识,正确做法是把这节车厢卸下来,让它等下一班车——而不是拦下整列火车。

2019 年他们卸了车厢。2026 年,九个国家选择拦火车。

被一起拦下的是什么?reflection、std::execution、库硬化,以及 contracts 本身。reflection 是这一版最受期待的东西——GCC 16.1 早在 2026 年 4 月就实现了 P2996R13,-std=c++26 -freflection 就能用。现在它躺在一份 ISO 没有批准的草案里。

这就是捆绑投票的结构性问题:标准是一列火车,所以任何一节车厢的争议都能让整车晚点;而唯一能精确卸车厢的工具,在 WG21 手里,不在国家体手里。 国家体没有”只删 contracts”这个选项,它只有”通过”和”不通过”。

于是为了阻止一节车厢,必须扣下整列车。

四、共识没有传递性

把三次表决排在一起,会看到一件很奇怪的事。

2025 年 2 月 15 日,WG21 全体会议把 P2900R14 合入 C++26 工作草案,据报道是 100 票赞成、14 票反对、12 票弃权——只算正反两方,接近 88%。2026 年 3 月,WG21 对含 contracts 的完整草案表决,据 Andreas Fertig 的说法是”大幅多数”通过,达到足以送交 DIS 的门槛。2026 年 10 月,国家体投票:60%。

同一个技术判断,经过三个投票房间,得到 88%、三分之二以上、60% 三种结论。共识没有传递性。

原因不复杂:投票的人换了。WG21 是几百个常年开会、彼此认识、下一次会议还要再见面的人。国家体是另一批人——各国标准化机构的成员,其中很多人一年只在邮件列表里出现几次。他们看到的不是三年的讨论过程,是一份 PDF 和一堆意见。

WG21 自己有一篇论文把这件事分析得极其冷峻,编号 P4195R1,标题叫《WG21 Game Theory: The Culture That Emerges From SD-4》。它说这个机制筛的是”能不能在委员会的否决结构里活下来”,而它无法判断活下来的东西是否优秀:

“The mechanism selects for survivability through the committee’s represented veto structure. It cannot tell whether the survivor was excellent.”

它对少数派的处境有一段更扎人的描述:多数派可以指着投票结果,作者可以指着几年的工作,主席可以指着共识,而少数派只能指着自己反对的理由;如果这些理由没有被保留在机构记录里,持这些理由的人就会认为,流程正在把”我没能说服这个房间”转换成”我的论证失败了”。

这段话解释了否决权为什么终究会被使用。在一个只记录结果、不记录理由的体系里,唯一有分量的动作是否决。 写论文是意见,会上发言是意见,投弃权是意见;按按钮是事实。

而按钮很便宜。25% 的反对票就够了;而且它不需要提出一个更好的方案,只需要不接受现有的方案。P4195R1 里有一句我很喜欢的话,讲理性参与者会区分两种表态:”我更喜欢 B”和”我愿意阻止 A 推进”——后者消耗的社会资本远多于前者。九个国家这次愿意付这笔社会资本。

五、一份会退信的邮件列表

Mateusz Pusz 是波兰国家体 KT 331 的成员。他写了一个比投票数字更让人不安的细节。

波兰内部的表决题目是:”是否否决 DIS 并向 ISO 提交技术意见。”这个动议没通过,所以波兰只能投弃权——而按规则,弃权不能附技术意见。一个国家因为内部程序没走完,在国际场合等于没有说话。

没走完的原因在这里:投票截止前几天,委员会还凑不够法定人数。有人换了工作,邮箱失效;Pusz 只能靠 LinkedIn 一个个找回来,有些地址直接退信。而法定人数的分母包含已经不再参与的人——他自己没有权限把这些人移出名单,办公室也不能替他移。

一个管着全世界 C++ 的标准体系,其投票合法性的最后一环,是一份没人维护的邮件列表。

这跟软件里的 stale config、没人再看一眼的告警、没人记得为什么存在的 cron job 是同一类东西:不报错,不消失,只在被依赖的那一刻暴露。区别是,软件里它让一个服务挂掉;这里它让一个国家的意见归零。

六、编译器不等 ISO

GCC 16.1 在 2026 年 4 月就发布了,已经实现 P2900R14 Contracts(实验性),reflection 需要 -std=c++26 -freflection,C++20 成为默认方言。也就是说:标准被否决了,编译器照跑。

所以”否决”的实际后果不是”你不能写”,而是“你不能依赖”。

这个区别决定了谁真的受伤:

  • 写业务代码的人:影响很小。打开 enforce 开关跑测试,体验接近 assert,只是语法好看些。
  • 写库的人:不敢把前置条件写进签名当作 ABI 契约,因为下游可能把它编成 ignore。契约退化回注释。
  • 写标准库的人:硬化没法建在它上面(P3878)。而硬化恰恰是当下最被需要的东西。
  • 2027 年要交付、需要 reflection 的团队:被一场跟自己无关的争议按住了日程。

我一直觉得,标准的价值从来不是”让编译器能做什么”,而是”让编译器不能随便做什么”。它是一份关于不确定性的约束:你写下的东西,在别人的机器上也成立。这次被否决的正是后者。

七、这件事跟不写 C++ 的你有关系

两条,都不是比喻。

第一条:默认值是接口设计里最贵的那个决定,而 contracts 把它交给了实现者。

pre(x > 0) 如果是”构建可以丢弃的提示”,它就是注释;如果是”程序必须满足的保证”,它就是契约。两者语法一模一样,语义差一个数量级。而 P2900 把选择权留给了编译器和编译选项——于是同一行代码的含义,取决于别人的构建脚本。

这条规律在任何地方都成立。Python 的 -O 会静默删掉所有 assert;TypeScript 的 strict 决定 null 是不是一个类型;JSON Schema 的 additionalProperties 默认允许未知字段;SQL 的 NULL 参与任何比较都返回 UNKNOWN 而不是 FALSE。凡是”默认值由使用方决定”的设计,最后都会分裂成两个互不兼容的世界。区别只在于,C++ 这次的裂缝发生在国际标准层面,被九张反对票记录了下来。

写接口的时候,我现在的习惯是:如果一个参数的默认值会改变语义而不只是改变行为,就不要给它默认值。 让它必须被显式写出来——哪怕多一行。

第二条:任何没有超时的共识协议,最终会被最不着急的一方控制。

DIS 投票没有”到期自动通过”,只有”失败后重新走流程”:修订稿重新投(八周)、退回委员会草案、降级为技术规范、或者取消项目。四条路都得在 WG21 里再拿一次多数。所以一个少数派不需要赢得辩论,只需要不接受结果,就能把日程无限后推。而多数派的成本是可见的(发布日期、下游计划、厂商承诺),少数派的成本是不可见的(反正现在也没有 reflection)。

这跟我 10 月写 Agent 停止条件时看到的是同一件事的两面:那边是循环没有退出条件,会一直消耗外部资源;这边是流程没有退出条件,会一直消耗别人的日程。人写的协议和机器写的循环,失效模式惊人地一致——都在等一个不会自己到来的终止信号。

11 月 16 日,布济奥斯

WG21 的下一次全体会议在巴西布济奥斯,11 月 16 日到 21 日,是结果公布后的第一次全员到场。

桌上的方案大致三种:再投一次(P4433R0 主张不删 contracts,并要求 WG21 驳回那些没有带来新技术的删除意见)、删掉 contracts 保住其余(P4238R1 作者之一 J. Daniel Garcia 在 Seacord 帖子下的意见就是这个)、或者把 contracts 降级成技术规范另发。

第二种最省事,也最可能。但它的代价不在文本里:一旦否决被证明有效,它就会变成常规手段。 2019 年卸车厢之所以可行,是因为所有人都相信下次还有机会——火车会再来。现在这份信任被消耗了一次,而重建它比删掉一个特性难得多。

Fertig 在文章里写,他认为这是一场”巨大的灾难”,无论你喜不喜欢 contracts。我同意一半:对 C++ 来说确实是灾难,但对”标准化到底该怎么运作”这个问题,它可能是一次迟到了很久的压力测试。二十八年没人按的按钮,不代表它不该存在;只代表没人知道按下去会发生什么。现在知道了。

ISO 规定,投票结果和全部意见要在 12 周内公布,也就是 2027 年 1 月中旬之前。在那之前,那九张反对票的具体理由都还只是推测——包括我上面这些推断。Pusz 也明确说了,他不会公开波兰内部的票数分布。

但有一件事已经确定了:那个按钮存在,而且现在所有人都知道它在哪。


参考与延伸

说明:ISO 规则限制投票明细对外公开,各国意见全文须在投票结束 12 周内(即 2027 年 1 月中旬前)发布。文中的票数(12/20、9/22)与国别名单来自多方一致的公开报道;”反对票意在 contracts”是 Pusz 与多家媒体的判断,尚待意见全文证实。P2900 在 2025 年 2 月的合入票数引自会议报道,未经 ISO 官方公布。

本文由作者按照 CC BY 4.0 进行授权

9000:1 与 16.4%:你的一行状态提示,比整场对话还贵

-