C++26 合约:前置条件和后置条件简介
在关于标准库加固的文章中,我们查看了对无效标准库容器访问和其他破坏性前置条件的检查。我提到这些要求是以“合约”的形式表达的。那么我们的代码呢?在这篇文章中,我们将探讨 C++26 中的合约。我们将从一个简单的 cassert 开始,将要求移到函数内部,并查看如何检查结果。我们还将查看声明和定义、编译器选项以及一些可能让你惊讶的细节。 从断言开始 让我们从一个小例子开始:首先是一个头文件: // foo.h #include <vector> int get_value ( const std :: vector < int >& values , std :: size_t index ); 实现: // foo.cpp #include "foo.h" // 我们的函数声明... #include <cassert> int get_value ( const std :: vector < int >& values , std :: size_t index ) { assert ( index < values . size ()); return values [ index ]; } 这个函数期望一个有效的索引。如果条件为假,assert 将打印诊断消息并终止程序。然而,这只有在启用断言时才会发生。定义 NDEBUG 将禁用检查,并基本上将检查从代码中移除。这个简单方法有什么问题?这个要求仅存在于实现内部。仅查看声明的人看到的是一个向量和一个索引,而不知道它们之间的关系。 添加前置条件 使用 C++26 合约,我们可以将该要求从函数体内的断言移到函数本身: int get_value ( const std :: vector < int >& values , std :: size_t index ) pre ( index < values . size ()) { return values [ index ]; } 注意参数列表后的新 pre(...) 语法。它引入了一个前置条件断言:期望在进入函数时成立的条件。更重要的是,我们可以将这个前置条件添加到声明中: int get_value ( const std :: vector < int >& values , std :: size_t index ) pre ( index < values . size ()); 这一特性是通过 P2900 引入的,即 C++ 的合约。让我们理解它是如何工作的,以及它与 assert 的不同之处。 为什么使用合约而不是 assert? 起初,pre(index < values.size()) 可能看起来像是另一种写法 assert(index < values.size())。两者都表示应该为真的条件。那么使用合约有什么好处呢?有几个重要的区别: - 声明中的要求:我们可以直接在函数声明中放置 pre 和 post。这使得任何阅读 API 的人都能看到要求,而无需查看实现。 - 更灵活的检查:传统的 assert 是通过 NDEBUG 启用或禁用的。合约定义了几种评估语义,包括忽略检查、报告违反并继续或终止程序。编译器的实现决定了可用的语义以及如何选择它们。 - 前置条件和后置条件:合约区分了调用者必须提供的内容和函数承诺返回的内容。使用 assert,我们必须手动在函数体内放置检查,包括在不同返回语句之前的检查。这也使得合约对于文档 API 变得有用,即使运行时检查被禁用。 完整的前置条件示例 这是一个完整的示例(为了简单起见现在没有头文件): #include <print> #include <vector> int get_value ( const std :: vector < int >& values , std :: size_t index ) pre ( index < values . size ()) { return values [ index ]; } int main () { std :: vector < int > values { 10 , 20 , 30 }; std :: println ( "{}" , get_value ( values , 1 )); // get_value(values, 10); // 违反了前置条件 } 有效调用打印:20 这里是您可以玩一下有问题调用的未注释版本: 在 GCC 上,我得到: 程序返回:134 程序 stdout 20 程序 stderr 合约违反在函数 int get_value(const std::vector<int>&, std::size_t) 中于 /app/example.cpp:6: index < values.size() [assertion_kind: pre, semantic: enforce, mode: predicate_false, terminating: yes] 没有活动异常的情况下调用终止 程序终止时信号 SIGABRT (6) 声明和定义中的合约 为了清晰起见,合约可以出现在声明中、定义中或两者都有: int get_value ( const std :: vector < int >& values , std :: size_t index ) pre ( index < values . size ()); 定义可以省略合约说明符: int get_value ( const std :: vector < int >& values , std :: size_t index ) { return values [ index ]; } 合约断言是由函数的第一次声明建立的。后来的声明或定义可以省略合约说明符,如上所示,或重复相同的序列。后来的声明不能引入不同的前置条件或后置条件。在实践中,如果一个函数在头文件中声明,那么第一次声明就是它的自然位置。
本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。
☕请我喝杯咖啡