C++ 的 assert 宏已经六十多岁了。它来自 C,功能极其有限:没有前置条件和后置条件的区分,没有标准化的违反处理,NDEBUG 一定义就彻底消失。P2900(Contracts for C++)改变了这一切。
2025 年 2 月,ISO C++ 委员会在哈根堡会议上正式将 P2900R14 纳入 C++26。这是 SG21 合同研究组前后八年的设计成果。提议者名单包括 Joshua Berne、Timur Doumler 和 Andrzej Krzemieński,几乎是 C++ 社群在契约设计领域的全部核心人物。
设计动机很直接。assert() 只能表达 “某个条件必须成立”,但无法分清楚这个条件是谁的责任。调用者传了非法参数,还是实现者返回了错误结果?assert(n > 0) 说不清楚。Contracts 用 pre 和 post 把这个责任归属直接写进了函数签名里。
三剑客:pre、post、contract_assert#
P2900 定义了三类契约断言:
| |
pre (condition) 声明前置条件,在函数体执行前检查。违反时责任在调用者。post (condition) 或 post(r: condition) 声明后置条件,正常返回后检查。r 是结果绑定,你可以在条件中引用函数的返回值。contract_assert (condition) 是函数体内的通用断言,等价于旧 assert() 但有标准化语义。
三者可以混合顺序。同一个函数可以有多个 pre 和 post:
| |
这个声明一读就懂:调用前确保有空间,调用后大小加一,新元素在尾部。如果违反其中任何一条,框架会给出准确的信息,告诉你违反的是哪条断言、语义是什么。
需要注意,post 的参数引用需要显式 const。按值传递的参数如果在 post 中被引用,必须标记为 const。这在头文件签名的长期维护中是个好习惯。
四种评估语义#
每个断言采用哪种语义由实现决定。标准划了四个等级:
| 语义 | 是否检查 | 违反后行为 |
|---|---|---|
ignore | 不检查 | 无效果 |
observe | 检查 | 调用 handler,执行继续 |
enforce | 检查 | 调用 handler,然后合约终止 |
quick-enforce | 检查 | 立即合约终止 |
ignore 把断言编译掉,效果类似 NDEBUG 但走标准路径。observe 适合测试:你得到通知,程序继续跑。enforce 和 quick-enforce 是生产环境的看门人,区别在于 enforce 允许 handler 介入(记录日志后再终止),而 quick-enforce 裸终止,开销最小。
同一个断言在同一次执行中可能使用不同语义。标准设计上容忍了这个自由度。实现可以基于编译选项、函数属性、甚至是运行时的动态选择。
这种设计不是过度工程。想象一下:你在调试阶段用 observe 捕获所有违规,在 CI 中用 enforce 确保零容忍,在线上用 quick-enforce 把性能损失降到零。同一个断言,三个环境,三种语义。
契约违规处理#
断言违反时调用 std::contracts::handle_contract_violation(const contract_violation&)。
contract_violation 对象提供以下信息:
assertion_kind():pre、post还是assertevaluation_semantic():当前使用的评估语义is_terminating():这个 violation 是否会导致终止evaluation_exception():若谓词因异常退出,捕获异常指针comment():实现定义的人可读描述
你可以替换默认 handler,前提是实现允许(标准把替换性设为 implementation-defined)。安装自定义 handler 的一个场景:在生产环境中记录所有 observe 语义下的违规,然后上报到监控系统,而不是默默吞掉。
P2900 还引入了 mixed mode 的概念:同一个翻译单元内的不同函数可以使用不同的评估语义。这意味着你可以在一个模块中开启严格检查,另一个模块中完全跳过,而不需要全局编译开关。这种粒度控制在实际工程中非常实用——核心模块的契约检查可以始终保持开启,边缘模块的性能敏感路径可以走 ignore。
代码实战#
写一个带契约的向量查找函数:
| |
这个 post 保证了返回值要么是 -1,要么是合法索引。任何人看到签名就知道函数承诺什么,不需要读函数体。
安装自定义违规处理 handler:
| |
后置条件的结果绑定可以自定义名字。一个常见的困惑是:为什么不用 auto 让编译器推断?提案者给出了明确的理由:显式命名可以让后置条件在不同编译单元间保持一致,这在 LTO 和分离编译场景下至关重要。post (r : r > 0) 中的 r 就是你命名的左值,引用函数的返回值对象。这在多返回路径的函数中特别有用。
一个经典例子,阶乘函数:
| |
没有契约的时候,你只能写 “n must be non-negative” 在注释里。现在编译器知道这条约束,运行时可以验证,读者看一眼签名就明白。
与现有 C++ 特性的关系#
Contracts 不是凭空出现的。它与 C++ 已有的语言设施形成互补:
- 与 noexcept 的关系:
noexcept说 “这个函数不会抛异常”,pre/post说 “这个函数在满足条件时行为正确”。两者可以共存在同一个函数签名上。 - 与概念(concepts)的关系:Concepts 约束类型参数,Contracts 约束运行时值。
std::sort(Sortable auto& rng)要求rng的元素类型支持比较;pre (!rng.empty())要求范围不空。一个是编译期类型约束,一个是运行时值约束。 - 与异常的关系:Contracts 设计的初衷之一就是替代用异常做参数校验的惯用法。
if (x < 0) throw std::invalid_argument(...)与pre (x >= 0)在语义上等价,但后者是声明式的,不涉及异常控制流。
生态现状#
| 编译器 | C++26 Contracts 状态 | 备注 |
|---|---|---|
| Clang | 实现中 | 实验性支持在 trunk,追踪 P2900 |
| GCC | 未开始 | 等待 C++26 正式发布 |
| MSVC | 评估中 | 表态会跟进 |
| Apple Clang | 跟随上游 | 等 Clang 稳定后合并 |
| EDG eccp | 已支持 | 实现了 P2900R14 全部内容 |
Feature-test 宏:__cpp_contracts == 202502L,__cpp_lib_contracts 对应标准库部分。
Contracts 的推动力很强。除了 P2900 外,SG21 还在并行推进多个扩展提案:P3097(虚函数的契约)、P3098(后置条件的捕获语法)、P3099(用户自定义诊断消息)、P3290(将现有 assert 与 Contracts 集成)、P3400(断言属性控制)。C++29 中 Contracts 还会继续扩展。
今日可执行动作#
- 关注 Clang 的 contracts 实现进度。在即将发布的 Clang 20/21 中尝试
-fcontracts实验标志,试用pre/post语法看编译错误提示是否清晰。 - 把现有项目中的
assert()换为contract_assert()。配合observe语义收集生产环境中的违规数据。你可能会惊讶于有多少隐藏的 API 误用。 - 用
pre重构核心 API 的参数校验。与其在注释里写 “参数不能为空”,不如直接写成pre (!vec.empty())。检查、文档、沟通一次完成。
参考#
- P2900R14 Contracts for C++. Joshua Berne, Timur Doumler, Andrzej Krzemieński (2025-02). https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p2900r14.pdf
- cppreference.com - Contract assertions (since C++26). https://en.cppreference.com/w/cpp/language/contracts
- P3846R1 C++26 Contracts, reasserted. Timur Doumler, Joshua Berne (2026-03). https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3846r1.html
- P4208R0 Info: C++ Contracts on Trial - Does P2900 Survive Cross-Examination?. Vinnie Falco (2026-05). https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p4208r0.html

