C++26契约是编译期声明而非运行时断言,需显式启用;它不改变ABI,仅向编译器和工具传递接口约束,前置条件约束调用方责任,后置条件限于参数、result和old表达式,错误使用将导致未定义行为。

Contracts 不是运行时断言,而是编译期契约声明
你写的 [[expects: x > 0]] 或 [[ensures: result > 0]] 在 C++26 中默认不生成任何运行时检查代码——它只是向编译器、静态分析工具和调用者传递明确的接口契约。是否启用检查、如何处理失败(抛异常 / 终止 / 忽略),由实现定义且需显式配置(如通过 -fcontracts=on 或 #pragma clang contract 等编译器开关)。
这意味着:API 设计者必须主动决定契约的“严格等级”,不能依赖它自动拦住错误输入;使用者也不能默认它会在生产环境生效。
- 契约本身不改变函数签名或 ABI,
int f(int x) [[expects: x != 0]]和int f(int x)对链接器完全等价 - 静态分析器(如 clangd、CppCheck)可利用契约推导不可达路径、优化警告,但 IDE 不会自动高亮违反契约的调用点,除非你启用了对应插件支持
- 若未开启编译器契约支持,所有
[[expects]]/[[ensures]]被当作无关属性忽略,无警告、无错误
前置条件(expects)直接约束调用方责任
[[expects]] 把“输入合法”从文档注释升级为可机器读取的契约条款。但它不提供自动防御——它要求调用方在传入前确保条件成立,否则行为未定义(UB),就像解引用空指针一样严重。
典型误用是把它当运行时校验替代品:
立即学习“C++免费学习笔记(深入)”;
int divide(int a, int b) [[expects: b != 0]] {
return a / b; // 若 b == 0,UB 已发生,不是“抛 std::invalid_argument”
}
- 不要在库函数里写
[[expects: ptr != nullptr]]后再加if (!ptr) throw ...—— 契约与运行时检查混用会误导使用者,也违背契约语义 - 对用户可控输入(如 CLI 参数、配置文件值),应在入口层做运行时验证并给出友好错误,而非依赖
[[expects]]捕获 - 模板函数中使用
[[expects: std::is_integral_v是无效的:契约表达式必须在求值时有确定结果,类型特征不能出现在 expects 表达式中]]
后置条件(ensures)让返回值承诺可验证、可推理
[[ensures]] 描述的是函数执行后的状态,其表达式可访问参数(带 old 修饰符)、局部变量(仅限 const 变量或返回值别名)和返回值(用 result)。
常见陷阱是误以为它能观察所有副作用:
std::vectorsort_copy(std::vector v) [[ensures: std::is_sorted(result.begin(), result.end())]] [[ensures: result.size() == old(v.size())]] { std::sort(v.begin(), v.end()); return v; }
-
old(v.size())合法,因为v是按值传入,old可捕获其进入函数时的状态 - 但
[[ensures: v.empty()]]非法:v是局部对象,非 const,且未被声明为const,不能在 ensures 中访问 -
[[ensures: !result.empty() || input_was_empty]]不行:input_was_empty是未声明的标识符,ensures 表达式作用域极窄,只认参数、result、old形式及函数内 const 变量
API 版本演进时 Contracts 会暴露兼容性断裂点
给已有函数添加 [[expects]] 是二进制兼容但源码不兼容的变更:原本合法的调用(如传入负数给新加上 [[expects: x >= 0]] 的函数)将触发 UB。这比加 const 更隐蔽——没有编译错误,只有未定义行为。
- 发布带契约的公开 API 前,必须同步更新文档、示例和测试用例,明确标注哪些输入从此变为“禁止”
- 不建议在稳定 ABI 的 shared library 接口中轻率引入契约,尤其当客户端可能绕过头文件直调符号时——契约信息不参与符号导出,调用方根本看不到
- 若需渐进增强契约,可用宏控制:
#ifdef CONTRACTS_ENABLED [[expects: x > 0]] #endif,但需确保构建系统统一管理该宏定义
真正棘手的地方在于:契约既不是语法强制,也不是运行时护栏,而是一种需要设计者、实现者、使用者三方共同理解并遵守的隐式协议。写错一个 old 表达式,或漏掉一个边界 case,不会报错,只会让某次重构后某个边缘调用突然变成 UB。











