代表性面试主题

C++26 Contracts:如何把前置条件、后置条件与 contract_assert 安全落地?

编程题困难
Offer.cc 编辑团队发布 更新

题干

如何解释 C++26 的 pre、post 与 contract_assert,并在编译器支持不完整时安全迁移现有 assert?

题干与适用场景

你负责一个订单库,准备采用 C++26 Contracts 表达 API 的前置条件、后置条件和函数体内不变量。请说明 prepostcontract_assert 的语义边界、评估模式、违约处理,以及如何从现有 assert 迁移而不把不可信输入校验遗漏。回答应覆盖编译器尚未完整支持时的发布策略。

面试官考察点

  • 能否区分契约文档、运行时诊断和业务输入校验。
  • 能否解释谓词可能被省略或重复评估,因而避免副作用。
  • 能否设计 violation handler、日志和故障隔离,而非假设失败必然抛异常。
  • 能否用编译器能力矩阵和灰度开关处理 C++26 支持差异。

澄清问题

  1. 这些 API 的调用者是受信任的库内部代码,还是直接接收网络请求?
  2. 生产环境要观察违约、终止进程,还是继续服务并隔离请求?
  3. 目标编译器、标准库版本和 ABI 发布周期分别是什么?

30 秒回答框架

先按责任边界回答:pre 描述调用者必须满足的条件,post 描述函数返回时的承诺,contract_assert 描述函数体内的局部契约。再说明谓词必须无副作用,因为实现可选择省略或重复评估;违约交给统一 handler 和部署策略。最后补充不可信输入仍要显式校验,并用特性检测、编译器矩阵和灰度发布控制迁移风险。

分步骤深入解答

1. 建立契约层次

前置条件属于调用者责任,后置条件属于被调用函数责任。它们适合表达 API 的可组合约束,例如容量必须为正、返回值满足排序关系。函数体中的 contract_assert 用于局部不变量或算法阶段性假设。三者都应能被代码审查者读懂,并和错误处理策略分离。

2. 写无副作用谓词

谓词只读取状态并计算布尔结果。不要在谓词里递增计数器、修改缓存、释放资源或依赖一次性随机值。C++26 契约语义允许实现选择不同评估模式,某些模式可省略评估,另一些模式可能多次评估;副作用会使正确程序的行为依赖构建选项。

cpp
int withdraw(Account& a, int amount)
  pre (amount > 0)
  pre (amount <= a.balance())
  post (a.balance() == old_balance - amount); // old_balance 需由设计支持
{
  contract_assert(a.is_open());
  return a.debit(amount);
}

示例中的 old_balance 只是说明设计问题;C++26 当前设施没有通用的 postcondition capture,不能假定存在 old(...) 语法。需要旧值时应在函数体显式保存,并确认该保存本身不改变业务语义,或等待后续标准扩展。

3. 选择评估与违约处理

工程上至少定义 observe、enforce 等模式对应的行为:开发和测试阶段收集完整诊断,关键服务在 enforce 模式下快速失败,线上请求边界则由上层把错误转成可观测的失败结果。不要把“违约一定抛异常”写进接口承诺;具体行为取决于实现和构建配置。handler 应记录契约位置、请求关联 ID 和版本,并避免递归调用同一契约路径。

4. 把输入验证留在边界

网络字段、用户金额和权限都属于不可信输入。先用显式校验返回可预期的业务错误,再调用带契约的内部函数。契约可以捕获内部调用者的编程错误,却不能替代认证、授权、限流或格式检查,也不能作为数据清洗的唯一防线。

5. 规划迁移与发布

用特性宏和编译器版本建立能力矩阵,分别验证语法、handler、调试信息和优化构建。先在单元测试和 canary 启用观察模式,比较违约率与性能,再逐步提高 enforcement。传统 assert 的宏展开、NDEBUG 和副作用假设不能直接等价迁移;需要逐点审计并保留原有失败语义,必要时通过适配层统一上报。

高质量示范回答

我会把 Contracts 当作可执行的设计约束,而不是输入校验或异常系统。pre 约束调用者,post 约束被调用函数,contract_assert 约束函数体内阶段性不变量。所有谓词都保持纯读,因为实现可以省略或重复评估;我会禁止 I/O、计数器修改和资源释放。违约通过统一 handler 产生结构化诊断,再由构建配置决定观察、强制或快速失败策略,代码不假定一定抛异常。请求入口仍先做认证、授权和不可信数据校验。迁移时建立编译器能力矩阵,先测试和灰度,再逐步开启 enforcement;传统 assertNDEBUG 和宏副作用逐项清理,不能机械替换。这样既能得到稳定的内部不变量检查,也不会把线上恢复策略寄托在未统一的实现行为上。

常见错误

  • pre 当成所有输入的安全校验,忽略边界层责任。
  • 在谓词里写日志、递增指标或修改对象,导致重复评估产生不同结果。
  • 断言违约一定抛异常或一定终止进程,忽略实现和构建模式。
  • 直接把 assert 搜索替换为 contract_assert,遗漏 NDEBUG、宏参数和副作用。
  • 使用不存在的通用 old(...) 语法描述 C++26 后置条件。

追问及应对

如果面试官问“为什么不全用异常?”

契约首先表达调用者与被调用者的责任,能参与静态审查和构建期策略;异常则是控制流和恢复协议。两者可以协作,不能互相替代。

如果问“谓词被重复执行会不会很慢?”

因此要先限制谓词为纯读、低成本表达式,并按构建模式测量开销。昂贵检查可放到显式诊断路径,而不是把副作用藏进契约。

如果问“虚函数能否直接写契约?”

C++26 当前契约断言对虚函数仍有边界,WG21 的后续路线图把虚函数支持列为扩展方向。设计多态 API 时应查目标编译器和标准版本,避免把后续提案当成 C++26 已有能力。

公开来源

同类题目

相关面试工具

用 Screenshot 处理算法题

截图题目后,按顺序看约束、解法、代码、边界条件和复杂度。

查看工具