摘要

C++26 周期, SG21 紧密地跟随了 P2965R1 的计划, 撰写了面向 C++26 的 C++ 标准契约设施的最小可行性提案 (Minimal Viable Product): P2900R10 - Contracts For C++. 本文作为该提案的补充, 介绍了该 MVP 提案背后的演变与逻辑, 同时讲解了其中的部分设计决策.

概述

契约 MVP 提案 介绍了契约的基本术语, 阐述了其应用范围和具体设计, 并给出了相应的 wording, 但对一些非常重要的背景, 如其中的设计决策, 考虑过的替代方案, 以及各个部分的来龙去脉, 并没有完全的展示. 本文补充了这些细节, 可以帮助大家更好地理解契约 MVP 提案,尤其能为那些希望在现有基础上进一步贡献力量的人提供参考.

本文分为两部分. 第一部分是 "概述", 主要包含了该提案的设计初衷, 我们希望通过它实现的目标, 以及对其他编程语言中相关技术的介绍,当然, 还有 C++ 标准化契约的一些历史. 第二部分是 "提议设计", 与 P2900R10 一小节一小节地对应, 包含了设计动机与尽可能完整的设计决策历史, 并引用了大量由 WG21 和 SG21 成员为此撰写的论文.

任何在 SG21 中达成了共识, 并影响了 P2900R10 的论文, 我们都会在此进行重点参考, 对它的动机和使用场景进行深入探讨, 以更好地理解它们的初衷. 有些设计决策是在 SG21 或其他相关 WG21 小组中讨论并确定的, 我们也会在这里总结这些讨论, 并附上相关的投票结果.

本文统一使用 P2900R10 第 2 节中定义的术语来指代与契约相关的概念和实体. 引用的许多论文或其他契约相关文献可能使用了不同的术语, 为了方便理解, 我们会在必要时进行特别说明.

契约的目的

P2900R10 第 2.1 节对契约进行了形式上的描述, 对 自然语言契约契约断言 两个概念进行了辨析, 同时将 契约设施 定义为 "一种支持用户编写契约断言, 并指定断言的不同种类 (如前置条件断言, 后置条件断言) 的语言特性". 然而, 它并没有回答一个问题: 我们为什么要设计契约?

P2900R10 的描述, 契约断言的主要目的是在 C++ 程序的一定位置 (尤其包含调用函数, 从函数返回时), 允许程序员用它表述在此处对程序正确性的期望, 并且在程序求值时对上述期望进行可手动开关的验证, 进而能够 暴露程序缺陷, 同时提供灵活, 可迁移, 扩展性强的, 对程序缺陷进行缓和的手段.

当然, 我们也可以这样描述 P2900R10 的目标: 用提出的契约断言, 取代 C 中的 assert 及相关 断言宏 并取得额外的收益, 如:

  • 不依赖宏进行断言, 可以解决相关的 parsing 问题[1], odr 问题[2], 代码腐败[3], 以及被关闭 / 忽略的断言对语言周边工具不可见的问题.

  • 函数的前置条件断言与后置条件断言可以在声明时提供, 而不是函数体内; 不必仔细检阅函数的具体定义, 就能获得函数相关的契约断言. 这让断言更加实用, 对人, 编译器, 周边工具 (如IDE, 静态分析) 都是如此.


1. C++ 中的宏直接从 C 继承而来, 一些 C++ 特有的语法, 如大括号初始化, 模板参数, 并不能被正确地理解. Widget{1, 2}array<int, 3> 这样的表达式会使断言的 parsing 出现错误.
2. 在公用头文件中的内联函数包含断言宏时, 包含该头文件, 并对该断言宏的开关情况不同的翻译单元一旦被编译, 链接, 就会违反 odr.
3. 关闭的断言宏会被预处理器直接忽略, 也就意味着这种情况下, 就算该断言包含了未定义标识符, 或其他各种无法运行的代码, 程序仍然能正常编译运行. 然而, 这段 "腐烂" 掉的代码总有一天要再被打开.