c++26的Contracts (契约) 对API设计意味着什么? (前置/后置条件)

1次阅读

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

c++26的Contracts (契约) 对API设计意味着什么? (前置/后置条件)

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::vector sort_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 表达式作用域极窄,只认参数、resultold 形式及函数内 const 变量

API 版本演进时 Contracts 会暴露兼容性断裂点

给已有函数添加 [[expects]] 是二进制兼容但源码不兼容的变更:原本合法的调用(如传入负数给新加上 [[expects: x >= 0]] 的函数)将触发 UB。这比加 const 更隐蔽——没有编译错误,只有未定义行为。

  • 发布带契约的公开 API 前,必须同步更新文档、示例和测试用例,明确标注哪些输入从此变为“禁止”
  • 不建议在稳定 ABI 的 shared library 接口中轻率引入契约,尤其当客户端可能绕过头文件直调符号时——契约信息不参与符号导出,调用方根本看不到
  • 若需渐进增强契约,可用宏控制:#ifdef CONTRACTS_ENABLED [[expects: x > 0]] #endif,但需确保构建系统统一管理该宏定义

真正棘手的地方在于:契约既不是语法强制,也不是运行时护栏,而是一种需要设计者、实现者、使用者三方共同理解并遵守的隐式协议。写错一个 old 表达式,或漏掉一个边界 case,不会报错,只会让某次重构后某个边缘调用突然变成 UB。

text=ZqhQzanResources