演讲嘉宾:吴咏炜(奇点智能研究院首席技术咨询师,《C++ 实战:核心技术与最佳实践》作者)
演讲主题:契约式编程——C++26 契约及其他
大会:2026 C++ 及系统软件技术大会 · 北京
一、引言:迟到四十年的原生契约
“契约式设计”(Design by Contract,DbC)的概念由 Bertrand Meyer 在 1980 年代提出,至今已有四十余年。它的思想朴素而深刻:一段代码的调用者与被调用者之间,存在一份隐含的契约——调用者保证前置条件成立,被调用者保证后置条件成立。
然而,直到C++26,这门诞生于 1985 年的语言才首次获得对契约的原生语言支持。吴咏炜作为国内知名 C++ 专家、《C++ 实战》作者,将在这届大会深入剖析这一"多年来最具争议的 C++ 语言特性"。
二、Design by Contract 的核心思想
DbC 的三大支柱是:前置条件(precondition)、后置条件(postcondition)、不变量(invariant)。
| 契约类型 | 语义 | 责任方 | 违反时意味着 |
|---|---|---|---|
| 前置条件 | 进入函数前必须为真 | 调用者 | 调用方的 bug |
| 后置条件 | 函数返回后必须为真 | 被调用者 | 实现方的 bug |
| 不变量 | 对象生命周期内恒为真 | 双方 | 状态被破坏 |
一个经典的除法例子,可以清晰呈现契约的价值:
// 未使用契约:问题潜伏在运行时深处intsafe_divide(intdividend,intdivisor){if(divisor==0){throwstd::invalid_argument("divisor must not be zero");}returndividend/divisor;}// 使用 C++26 契约:把约束显式化到接口intdivide(intdividend,intdivisor)[[pre:divisor!=0]][[post r:r*divisor==dividend]]{returndividend/divisor;}契约与传统防御式检查的本质区别在于:契约写的是"接口的承诺",而不是"实现的补救"。它把"谁该为这个错误负责"这个问题,从运行时追溯变成了编译期可读的文档。
三、C++26 契约的语法与语义
C++26 契约引入了几种核心语法形式。
3.1 前置与后置条件
#include<contract>voidpush_back(constT&value)[[pre:size()<capacity()]][[post:size()==old_size+1]]{// ...}后置条件可以绑定返回值(如[[post r: ...]]),也可以引用函数进入时的旧值(old_size)。
3.2 契约断言
voidprocess(intlevel){contract_assert(level>=0);// ...}contract_assert与经典的assert不同:它属于契约体系,可以被统一配置违反行为,且不能被NDEBUG静默移除。
3.3 契约违反的三种处理方式
契约"违反后怎么办"是整个特性最激烈的争议焦点。C++26 最终提供了三种可配置的语义:
契约违反语义(violation handling) ├─ ignore :忽略违反,继续执行(发布模式常见) ├─ throw :抛出异常(便于测试捕获) └─ eval_and_abort:求值并终止(最严格,防御越界)| 语义 | 适用场景 | 风险 |
|---|---|---|
| ignore | 生产环境、性能敏感 | 契约形同虚设 |
| throw | 测试、可恢复场景 | 可能掩盖早期错误 |
| eval_and_abort | 安全攸关、防御攻击 | 进程直接终止 |
争议的核心在于“契约违反是否应允许继续执行”。支持者认为,契约的本质是"文档化假设",违反契约是编程错误,任何继续执行都不可信;反对者则认为,在大型存量系统中,激进终止会导致线上事故,需要"温和降级"。
四、争议:为什么这是"最受争议"的特性
吴咏炜在议题中特别强调了这个特性的争议性,其历史确实充满反复:
- 语法之争:从
pre(...)函数体语法,到[[pre: ...]]属性语法,标准委员会反复横跳,属性语法的采纳曾被视为对"属性不影响语义"原则的打破 - 违反行为之争:
continue(继续)语义因安全问题被移除,eval_and_abort与throw的取舍长期悬而未决 - ABI 与 ODR 影响:契约是否参与函数签名、是否影响重载,涉及海量存量代码的兼容性
- 与 assert 的关系:契约是否会取代传统
assert,抑或并存,社区意见分裂
这些争议并非学术清谈,每一项都直接关系到数以亿计的存量 C++ 代码的演化路径。
五、工程实践:如何真正用好契约
抛开语法细节,吴咏炜强调的核心是:契约改变的是代码的"设计"方式,而非"检查"方式。
5.1 在接口处写契约,而非实现处
好的契约写在公共接口上,让调用者无需阅读实现即可理解约束;糟糕的契约散布在实现深处,等同于变形的断言。
5.2 用契约表达"不可能",而非"异常"
契约适合表达"如果这里为假,说明代码逻辑本身有 bug"这类编程错误;而资源耗尽、网络失败等运行期异常仍应使用异常机制。
// 正确:契约表达"调用者必须先初始化"voidconnect(Channel&ch)[[pre:ch.is_ready()]];// 错误:把可预期的运行期失败塞进契约voidsend(Channel&ch,constPacket&p)[[pre:!ch.is_full()]];// 网络拥塞是常态,不应终止进程5.3 渐进式引入
对存量项目,可以从最关键的公共接口开始引入前置条件,配合throw语义在测试环境运行,逐步暴露历史遗留的调用错误,再平滑过渡到更严格的语义。
六、契约与测试、静态分析的协同
契约并非孤立存在,它与单元测试、静态分析共同构成"三重防线"。
| 手段 | 检查时机 | 检查对象 | 与契约的关系 |
|---|---|---|---|
| 契约 | 运行时(可配置) | 接口约束 | 表达"应然" |
| 单元测试 | 测试阶段 | 行为正确性 | 验证"实然" |
| 静态分析 | 编译/CI 阶段 | 潜在缺陷 | 提前发现 |
三者的分工清晰:契约表达接口的应然约束,单元测试验证具体行为,静态分析在运行前拦截潜在缺陷。
// 契约 + 静态分析的配合示例intindex_of(conststd::vector<int>&v,inttarget)[[pre:!v.empty()]][[post r:r==-1||v[r]==target]]{for(inti=0;i<static_cast<int>(v.size());++i)if(v[i]==target)returni;return-1;}前置条件!v.empty()让调用者必须保证容器非空,后置条件则约束了返回值的语义。配合 clang-tidy 等静态分析工具对"调用前未检查空容器"的告警,可以在 CI 阶段就把多数契约违反拦截在提交之前。
吴咏炜强调:契约的最终价值,是让这些约束从人的记忆里、从散落的注释里,沉淀为编译器可读、可执行、可审计的代码。
七、总结
四十年前提出的 Design by Contract,终于以契约(Contracts)的形式原生进入 C++26。它的价值不在于"多了一种断言",而在于把接口约束从隐式约定提升为显式契约,让"更健壮的代码"从口号变成可执行的设计准则。
2026 年 C++ 及系统软件技术大会上,吴咏炜将以三十年系统级开发经验,为开发者拆解这一最具争议特性的来龙去脉与落地之道。
大会信息
2026 奇点智能技术大会 + C++ 及系统软件技术大会
时间:2026 年 11 月 20-21 日
地点:中国·北京万达文华酒店
参会报名:https://boolan.com/enroll/c1051/event/1162?channel=seo
立即报名,与 C++ 实战专家面对面交流!