- Published on
C++26 契约(Contracts) — 以 114 票对 12 票通过的特性,以及唯一的实现 GCC 16.1
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 引言 — 并非全票通过的 C++26
- 契约是什么 — 三分钟讲清楚
- 核心设计 — 是否检查,源代码说了不算
- 反对方的论证 — 头文件里的一个 inline 函数
- 支持方的反驳 — 功能安全和语言安全是两回事
- 在 Kona 会议上修好的,以及最终没能进去的
- 编译器的现实 — 只有 GCC 16.1 一个
- 那么,什么时候该用、什么时候不该用
- 结语
- 参考资料
引言 — 并非全票通过的 C++26
2026 年 3 月 28 日星期六,在英国伦敦的克罗伊登(Croydon),ISO C++ 委员会结束了 C++26 的技术工作。在处理完夏季 CD(委员会草案)国际投票带来的 411 条国家机构(NB)意见之后,委员会敲定了将送交最终批准投票(DIS)的文档。
然而,最后一次全体大会表决并非全票通过。
赞成 114,反对 12,弃权 3
赫伯·萨特(Herb Sutter)在克罗伊登行程报告中写道,这种不一致「主要是因为对契约持续存在的担忧」。这里有个可比较的对象。2025 年 2 月,在哈根贝格(Hagenberg)将契约纳入 C++26 工作草案时的表决是赞成 100、反对 14、弃权 12。一年过去,反对票几乎没变,从 14 变成 12,但弃权票从 12 降到了 3。萨特的解读是这样的 — 弃权票异常地少,说明无论赞成还是反对,大多数专家如今都对自己的技术判断有了把握。
一项标准特性走到这个阶段还能收到两位数的反对票,这并不常见。我们来梳理一下究竟发生了什么,以及现在能不能用上这项特性。
顺带一提,本文只把萨特的行程报告当作表决数字的出处来使用。他站在支持契约的一方(还是 P3911R2 的共同作者),其中的解读与叙事框架都是他本人的。论证本身则直接取自双方各自撰写的原始论文。
契约是什么 — 三分钟讲清楚
契约断言(contract assertion)是一种语言语法,用来写函数的前置条件、后置条件,以及函数体中间的断言。形式有三种。
// 前置条件: 调用者必须遵守的东西
int divide(int a, int b)
pre (b != 0);
// 后置条件: 函数所保证的东西。返回值可以命名
int abs_value(int x)
post (r: r >= 0);
// 函数体内的断言语句
void process(std::span<int> data) {
contract_assert(!data.empty());
// ...
}
拿它和 C 的 assert 宏一比,差异就很清楚了。assert 是预处理器宏,没法附加在函数声明上。前置条件只能写在函数体里,只看头文件的调用者根本看不到。契约附加在声明上,因此成了接口的一部分,工具可以读取,而且后置条件可以用名字引用返回值。
到这里为止都没有争议。争论从下面这一点开始。
核心设计 — 是否检查,源代码说了不算
C++26 标准草案的 [basic.contract.eval] 把求值语义定义成了四种。
ignore— 没有任何效果observe— 检查,失败就调用处理器,处理器返回后继续执行enforce— 检查,失败就调用处理器,处理器返回后终止执行quick-enforce— 检查,失败就不调用处理器、立即终止
observe、enforce、quick-enforce 是检查语义(checking semantics),enforce、quick-enforce 是终止语义(terminating semantics)。而下面这句话,正是决定这项特性性格的地方。
It is implementation-defined which evaluation semantic is used for any given evaluation of a contract assertion.
某个契约断言到底会不会被检查,是实现定义的。决定权不在源代码里写的内容,而在编译器和构建标志。紧接着的一条注释走得更远。
The evaluation semantics can differ for different evaluations of the same contract assertion, including evaluations during constant evaluation.
即便是同一个契约断言,每次求值时用的语义也可能不一样。标准把「全部翻译为 ignore」和「全部翻译为 enforce」都列为「推荐做法(Recommended practice)」的选项,并写明默认值应当是 enforce,但这只是建议,不是强制要求。
这里还有一层。
The evaluation A of a contract assertion using a checking semantic determines the value of the predicate. It is unspecified whether the predicate is evaluated.
即便用的是检查语义,谓词(predicate)是否真的被求值也是未指定的。标准自己给出的例子就是这样。
struct S {
mutable int g = 5;
} s;
void f()
pre ((s.g++, false)); // #1
void g() {
f(); // 即便 #1 使用检查语义,s.g 的自增也可能不会发生
}
如果谓词带有副作用,那这个副作用可能发生,也可能不发生。实现可以在得到相同值的前提下,执行一个没有副作用的替代求值。
支持方的论文 P3846R0 用三个属性来定义契约 — 对程序的正确性而言是冗余(redundant)的、可以独立地被检查、求值语义的确定与源代码无关。第三点正是有意为之的设计,不是 bug。
反对方的论证 — 头文件里的一个 inline 函数
反对方论文的标题一点也不委婉:P3835R0《Contracts make C++ less safe -- full stop》,作者是约翰·斯派塞(John Spicer,EDG)、维勒·沃蒂莱宁(Ville Voutilainen)、何塞·丹尼尔·加西亚·桑切斯,2025 年 9 月 3 日发表。论证的核心就是一个代码例子。
// z.h — 第三方库的头文件
inline void f(int x) {
contract_assert(x > 0);
}
// l1.cpp — 库的实现。用 quick_enforce 编译
#include "z.h"
void g() {
f(0); // 期望契约违反被检测出来
}
// c1.cpp — 客户端代码。用 ignore 编译
#include "z.h"
extern void g();
int main() {
f(1);
g();
}
这个库把自己的代码构建成始终以 quick_enforce 来检查。客户端则以 ignore 构建。因为 f 是 inline 函数,按 ODR 的规则只会有一份定义存活下来。所以链接之后到底哪种语义会真正生效 — 库作者原本想要的检查是否还活着 — 取决于编译器有没有在 l1.cpp 里把调用内联,以及链接器最终选中了哪个翻译单元的副本。用论文里的原话照搬过来,程序员没有办法知道哪次调用会用上哪种语义。
只要头文件里有带契约断言的 inline 函数或模板定义,这个问题随时都会出现 — 也就是说,几乎覆盖了所有头文件库。论文写道,模块(modules)也解决不了这个问题。它的结论是这样的。
Contracts reduce the safety of C++ and should be removed until a version that is strictly an improvement in safety is provided.
同一方向的论文还有更多。P3829R0《Contracts do not belong in the language》(大卫·奇斯诺尔、斯派塞、加布里埃尔·多斯·雷斯、沃蒂莱宁、加西亚),还有 P3573R0《Contract concerns》,作者名单里挂着比雅尼·斯特劳斯特鲁普(Bjarne Stroustrup)和达维德·范德沃德(David Vandevoorde)。这不是一次轻描淡写的反对。
国家机构的意见也跟了上来。P3846R0 附录整理的契约相关 NB 意见共 17 条,来自瑞典、西班牙、美国、法国、罗马尼亚、捷克、芬兰这 7 个国家机构。其中西班牙的 ES-046 被这样概括 — 关键检查可能被移除,这会打开一个安全漏洞,从而让一种新型供应链攻击成为可能。多条意见明确要求把契约从 C++26 中移除。
如果「供应链攻击」这种说法听起来有点夸张,不妨看看 P3829R0 举的例子。照 P3846R0 的概括原样转述 — 某个函数在一个翻译单元里用 quick_enforce 编译,在另一个里用 ignore 编译。在优化 quick_enforce 那一侧时,GCC 会假定契约检查是强制生效的,从而移除调用方那边的空指针检查。可到了链接阶段,最终被选中的却可能是 ignore 版本。于是契约检查没了,那个因为「相信契约检查会兜底」而被删掉的空指针检查也没了。消失的不是一个检查,而是两个。P3829R0 把这定性为 P2900 的一处设计缺陷,并主张要解决它,要么(1)把混合模式变成 ODR 违规,要么(2)大幅改动编译器的中间表示,要么(3)放弃一项核心优化。
对此支持方有一个反驳,公平起见有必要如实转述。P3846R0 写道,EWG 在哈根贝格审视过混合模式的相关担忧之后,拒绝为契约而改动 ODR,并且GCC 那个跨过程优化的问题,已经在邮件列表上被判定为「与契约无关的编译器 bug」,复现代码也已经上报给 GCC。也就是说,支持方把这个问题看作实现层面的 bug,而不是语言设计上的缺陷。
支持方的反驳 — 功能安全和语言安全是两回事
支持方的回应是 P3846R0《C++26 Contract Assertions, Reasserted》(2025 年 10 月 6 日)。论文由蒂穆尔·杜姆勒(Timur Doumler)、约书亚·伯恩(Joshua Berne)、加什佩尔·阿日曼(Gašper Ažman)领衔,共 22 人署名,其中包括 GCC 的杰森·梅里尔(Jason Merrill)和扬·桑多(Jan Sando)、libc++ 的路易·迪奥内(Louis Dionne),还有负责 Clang 实现的埃里克·菲塞利耶(Eric Fiselier)。论文逐条回应了 17 项「担忧」。
核心反驳从拆分术语开始。
- 功能安全(functional safety) — 程序在不造成伤害的前提下完成预期功能的可能性
- 语言安全(language safety) — 未定义行为的不存在,或者证明这种不存在的能力
论文的主张是,契约是为前者而生的,不是为后者而生的。断言一旦被检查就能抓住 bug,一旦被忽略就在没有运行时开销的前提下记录了意图。语义可以从外部配置,这不是缺陷,而是被广泛采用的前提条件。
对于跨翻译单元的语义不一致(P3835R0 的例子),论文是这样回应的。
Mixing translation units compiled with different build flags is an inevitable consequence of the C++ compilation model. (...) With C assert, mixed mode makes programs IFNDR, and with P2900, the behaviour is limited to one of the semantics incorporated into the program, which is a significant improvement. (...) The worst case is that an assertion will go unchecked, which by design cannot introduce a new bug into an existing program.
也就是说,最坏的情况不过是断言未经检查地溜过去,而这在设计上不会给既有程序引入新的 bug。论文也毫不含糊地指出,如果要求整个程序使用统一的语义,这个特性就会变成「无法在大规模场景下使用的东西」。头文件里的 f 如果被用在性能关键的循环里,除了 ignore 可能什么都承受不起;而在验证较少的上层代码里,quick_enforce 可能才是合适的选择 — 论文的逻辑是,一个支撑不了这种可变性的断言特性,是没用的。
论文还指出,让 pre 和 post 的语义永远都被检查这个提案,早已在东京会议上被表决否决,而且是以压倒性共识否决的。取而代之,论文把标签(labels,P3400R1)作为表达「必须检查」的路径,定位为C++26 之后的扩展。
把两边的主张摆在一起看,事实层面真正有分歧的地方其实很窄。行为本身没有争议。检查可能悄无声息地消失,这一点双方都认,源代码里没有办法强制它,这一点双方也都认。分歧在于,这到底该算「不是安全特性」,还是「损害了安全」。
在 Kona 会议上修好的,以及最终没能进去的
2025 年 11 月的 Kona 会议上,委员会用了周一、周二两天来处理契约相关的反馈。结果是这样的。
保留契约,但修复两个 bug — 这是以压倒性共识达成的。被采纳的修复之一是 P3878R1《Standard library hardening should not use the 'observe' semantic》,作者是沃蒂莱宁、乔纳森·韦克利(Jonathan Wakely)、斯派塞、斯蒂芬·T·拉瓦维(Stephan T. Lavavej)。一篇由反对方撰写的 bug 修复论文被采纳了,这本身也从侧面证明了反对意见是技术性的,不是政治性的。
这里有一个对照组。同一次会议上,trivial relocatability(P2786)被发现有一个来不及修的决定性 bug,结果从 C++26 中被移除了。这说明委员会不是因为不愿意砍掉特性才把契约留了下来。同一周,一个被砍掉了,另一个被留了下来。
而没能进去的东西也有。在 Kona,委员会决定把 P3911R0《[basic.contract.eval] Make Contracts Reliably Non-Ignorable》里第一种方案的思路,一直推进到 3 月的会议。作为对罗马尼亚 NB 意见(RO 2-056)的回应,提案想加入一种能在源代码里写下「这条断言必须被强制执行」的语法。提议的形式是这样的。
int divide(int a, int b)
pre! (b != 0); // 提议的语法: 始终强制执行。没有进入 C++26
论文一路推进到了 R2(2026 年 1 月 14 日),安德烈·亚历山德雷斯库(Andrei Alexandrescu)和赫伯·萨特也加入成为作者。但当前标准草案的语法里并没有这个东西。
precondition-specifier:
pre attribute-specifier-seq_opt ( conditional-expression )
postcondition-specifier:
post attribute-specifier-seq_opt ( result-name-introducer_opt conditional-expression )
[dcl.contract.func] 的语法里没有 pre!,[basic.contract] 里任何地方也都没有出现「non-ignorable」这个词。C++26 的 __cpp_contracts 取值,仍然停在 P2900R14 的 202502L。萨特自己在克罗伊登的报告里也写道,「这项特性既没有被添加,也没有被移除」。
归纳一下就是:C++26 没有给你任何一种方法,能在源代码里写明某个契约检查必须被执行。反对方论证的核心问题原封不动地留在了标准里,这就是那 12 张反对票的实质内容。
再补充一点,C++26 的契约还没法附加在虚函数上。这项能力(P3097R3《Contracts for C++: virtual functions》)进的是 C++29 草案(按GCC 状态表的标准是 __cpp_contracts >= 202606L),而且 GCC 目前还没实现。对于一个卖点是「在接口上写明前置条件」的特性来说,虚函数被漏在外面,是个相当大的缺口。
编译器的现实 — 只有 GCC 16.1 一个
这是对从业者而言最重要的部分。在 cppreference 的 C++26 编译器支持表上,Contracts(P2900R14)这一行,GCC 那一栏写着 16,而 Clang、MSVC、Apple Clang、EDG eccp、Intel C++ 这几栏全都是空的。
截至 2026 年 7 月,实现了 C++26 契约的正式发布版编译器只有 GCC 16 一个。GCC 16.1 在 2026 年 4 月 30 日发布,比标准定稿晚了一个月。
用法是这样的。
# 光靠 -std=c++26 打不开契约。需要 -fcontracts
g++ -std=c++26 -fcontracts main.cpp
# 选择求值语义 (默认是 enforce)
g++ -std=c++26 -fcontracts -fcontract-evaluation-semantic=quick_enforce main.cpp
把 GCC 16 手册和源码里的选项定义对照起来看,是这样的。
| 标志 | 取值 | 默认值 |
|---|---|---|
-fcontracts | 启用契约特性 | 关闭 |
-fcontract-evaluation-semantic= | ignore / observe / enforce / quick_enforce | enforce |
-fcontracts-client-check= | none / pre / all — 在调用方一侧插入检查 | none |
-fcontracts-definition-check= | on / off — 被调用方一侧的检查 | on |
-fcontracts-conservative-ipa | 限制跨过程优化 | 开启 |
-fcontract-checks-outlined | 把检查拆到单独的函数里 | 关闭 |
有个小陷阱 — 手册正文把取值写成了 ignored、observed,但编译器实际接受的字符串是 ignore、observe、enforce、quick_enforce(依据 GCC 源码 gcc/c-family/c.opt 里的枚举定义,默认值 Init(3) = enforce)。要是照着手册原样复制粘贴,就会碰上「unrecognized contract evaluation semantic」的报错。
违反处理器(violation handler)可以通过定义下面这个函数来替换。
#include <contracts>
void handle_contract_violation(const std::contracts::contract_violation& v) {
// 记日志之类的操作
}
这张表里最值得留意的是 -fcontracts-conservative-ipa。照搬 GCC 文档的说法,这个选项说的是这样一件事 — 「防止跨过程分析在某个 inline 函数的函数体对某个翻译单元可见时贸然采取行动,因为契约求值的条件可能因翻译单元而异,那样的行动可能是错的」。而且它默认就是开着的。
前一节里 P3829R0 那个删掉空指针检查的场景,说的正是这件事。反对方所说的「不同翻译单元的语义可能不一样,据此去做优化是危险的」,这个特性在 GCC 里,被具象化成了一个默认让跨过程优化变得保守的标志。-fcontract-disable-optimized-checks 的说明里出现「能避免检查步骤被意外消除(elision)的优化」这种表述,说的也是同一件事。
这该算哪一方的证据,看法会分歧。反对方读出的是「语言把这个负担甩给了编译器」,支持方读出的是「这是实现能扛得住的问题,而且实际上已经扛住了」。但有一点是明确的 — 这场争论不是抽象的哲学辩论,而是一个默认开启的编译器标志,实实在在地存在着。
顺带一提,同一个 GCC 16 也实现了反射(reflection,P2996R13),不过那需要单独加上 -freflection。而消除了「读取未初始化值」这一未定义行为的 P2795R5(erroneous behavior)也进了 GCC 16 — 和契约不同,这个提案没有争议,只要重新编译就能拿到收益。
那么,什么时候该用、什么时候不该用
先说不该用的情况。
- 不要用它来验证信任边界。这是最重要的一条。用户输入、来自网络的数据、权限检查 — 这些都不该写成契约。用
ignore构建就会消失,如果它在头文件的 inline 函数里,还可能不由你意愿地消失。信任边界的检查,请用if加显式的错误处理来写。就连 P3846 都写道,「已经有不可忽略检查的机制了 — 比如if语句」。这不是反对方的主张,而是支持方论文里的原话。 - 不要用带副作用的谓词。它是否会被求值是未指定的。
- 不要用在需要可移植性的代码里。截至 2026 年 7 月,这是 GCC 16 独有的特性。Clang 和 MSVC 的正式版里都还没有。
- 虚函数接口上目前还用不了。得等 C++29。
- 谓词内部,外层变量会变成
const,this会变成const指针(即 const 化)。调用非 const 操作的检查是编译不过的。
它能发挥价值的情况。
- 在内部代码库里,用来把前置条件写进接口文档。光看头文件就能知道契约的内容,这是
assert做不到的,而且即便用ignore构建,这份收益依然留存。 - 调试和测试构建用
enforce、发布构建用ignore的经典模式。这其实就是用更好的语法,做以前assert在做的事,实际上大多数早期采用大概都会落在这里。 - 在确定只用单一编译器(GCC)构建的内部服务里,所有翻译单元都用同一种语义编译。P3846 所说的「自然的初期策略」正是这个 — 每个翻译单元都用单一语义编译。不混用模式,P3835R0 的问题就压根不会出现。
也就是说,只要你能掌控整个构建流程,契约就能正常工作。麻烦出现在你把第三方库当头文件引入的那一刻,也出现在库作者想说「我这个检查必须始终开着」的那一刻。C++26 没有给他们把这句话写进源代码的语法。
结语
归纳一下:C++26 契约在 2026 年 3 月 28 日以赞成 114、反对 12、弃权 3 的表决结果留在了标准里。这是 20 年讨论、SG21 五年工作,以及曾在 C++20 中被拿掉一次的经历之后,得出的结果。这项特性本身是真实的 — 附加在声明上的前置、后置条件做到了 assert 做不到的事,四种求值语义也反映了实务中各不相同的真实需求。
与此同时,反对方的论证也是真实的。源代码没法决定检查会不会执行,头文件里的 inline 函数中,程序员没法知道哪种语义会真正生效。这不是 bug,而是有意为之的设计,正因如此,它不属于能被「修好」的那类问题。在源代码里写下强制要求的语法(P3911)一路推进到了 3 月,但没能进入 C++26,标签(P3400)则留作了 C++26 之后的课题。
所以,面对这项特性该抱持的态度是明确的。契约不是安全特性,而是 bug 检测工具。就连支持方也这么说 — 它是为功能安全而生的,不是为语言安全而生的。要是漏看了这个区分,把它读成「C++26 靠契约就变安全了」,那你会正好按照反对方警告过的方式受伤 — 因为你把一个可能被悄悄跳过的检查,放在了信任边界上。
而截至 2026 年 7 月,就算你想用,GCC 16.1 也是唯一的选择。萨特预计 C++26 的采用会很快,但就契约而言,现在能做的也只是拿 GCC 16 去试验。要放进可移植的生产代码里,得等 Clang 和 MSVC 跟上之后再说。
参考资料
- C++26 is done! — Trip report: March 2026 ISO C++ standards meeting (London Croydon, UK) — Herb Sutter — 表决数字(114/12/3、100/14/12)与 411 条 NB 意见的出处
- Trip report: November 2025 ISO C++ standards meeting (Kona, USA) — Herb Sutter — 保留契约的决定、P3878R1、trivial relocatability 的移除
- P3846R0 — C++26 Contract Assertions, Reasserted (Doumler、Berne、Ažman 等 19 人) — 支持方对 17 项担忧的回应,以及 NB 意见附录
- P3835R0 — Contracts make C++ less safe -- full stop (Spicer, Voutilainen, Garcia) — inline 函数的例子
- P3829R0 — Contracts do not belong in the language
- P3911R2 — Make Contracts Reliably Non-Ignorable (Neațu, Alexandrescu, Teodorescu, Nichita, Sutter) — 没能进入 C++26 的
pre!提案 - [basic.contract.eval] — 标准草案关于求值语义的规定
- [dcl.contract.func] — 契约说明符的语法
- C++ Standards Support in GCC — Contracts P2900R14 → GCC 16,
__cpp_contracts >= 202502L - GCC 16 Release Series — Changes 与 GCC C++ Dialect Options — 契约相关的标志
- GCC Releases — GCC 16.1 于 2026 年 4 月 30 日发布
- Compiler support for C++26 — cppreference — 除 GCC 16 外均不支持