40

我在 B. Stroustrup 的关于 C++17 的思考中阅读了有关合同的内容,并协助了一个小型演示文稿谈论它们,但我不确定我是否真的理解它们。

所以我有一些询问,如果可以用一些例子来说明它们:

  • 合约只是经典的更好的替代品,assert()它们应该一起使用吗?对于软件开发人员来说,哪些合同真的很简单?

  • 合同会对我们处理异常的方式产生影响吗?如果是,我们应该如何使用例外和合同?

  • 使用合约是否意味着执行时的开销?我们是否可以在发布代码上停用它们?

提案 N4415

Vector 类的索引操作符的前置条件契约可以写成:
T& operator[](size_t i) [[expects: i < size()]];

类似地,ArrayView 类的构造函数上的后置条件契约可以表示为: ArrayView(const vector<T>& v) [[ensures: data() == v.data()]];

感谢@Keith Thompson 评论:

合同没有进入 C++20一个新的研究组 SG21 已经成立。

4

3 回答 3

24

据我从本文档中阅读: http ://www.open-std.org/JTC1/SC22/WG21/docs/papers/2015/n4415.pdf

合约做了assert多年来一直试图以原始方式做的事情。它们既是文档又是运行时断言,说明调用者应该如何调用函数,以及调用者期望函数返回后代码处于什么状态。这些通常被称为前置条件和后置条件,或不变量。

这有助于清理实现方面的代码,因为有了合约,我们可以假设一旦执行进入你的函数,你的参数就处于有效状态(你期望它们是什么)。

后置条件部分可能会改变您处理异常的方式,因为使用合约您必须确保抛出异常不会破坏您的后置条件。这通常意味着您的代码必须是异常安全的,尽管这意味着强异常保证还是基本保证取决于您的条件。

例子:

class Data;
class MyVector {
public:
    void MyVector::push_back(Elem e) [[ensures: data != nullptr]]
    {
        if(size >= capacity)
        {
            Data* p = data;
            data = nullptr; // Just for the sake of the example...
            data = new Data[capacity*2]; // Might throw an exception
            // Copy p into data and delete p
        }
        // Add the element to the end
    }
private:
     Data* data;
     // other data
};

在此示例中,如果neworData的构造函数抛出异常,则违反了您的后置条件。这意味着您应该更改所有此类代码以确保永远不会违反您的合同!

当然,就像 合同一样assert,合同可能包括运行时开销。但不同之处在于,由于可以将合约作为函数声明的一部分,编译器可以进行更好的优化,例如在调用者的站点评估条件,甚至在编译时评估它们。本文开头提到的文档的第 1.5 节讨论了根据您的构建配置关闭合约的可能性,就像普通的旧断言一样。

于 2015-07-09T12:30:16.503 回答
10

我从提供的原始文档 OP中的链接开始。我想有一些答案。我强烈建议从那篇论文开始。这是 TL&DR 版本:

合约不是通用的错误报告机制,也不是测试框架的替代品。相反,当由于程序各部分之间的期望不匹配而导致程序出错时,它们提供了一种基本的缓解措施。合同在概念上更像是集成到语言中的结构化 assert(),遵循语言语义规则——因此是原则性程序分析和工具的基础。

关于您的问题:

  • 它是结构化的 assert(),所以是的,可以说在某些情况下 assert() 必须替换为合同。
  • 让我在这里使用另一个引用:

...合同的表达在逻辑上必须是操作声明的一部分。

和例子:

T& operator[](size_t i) [[expects: i < size()]];

在我看来,这很好而且可读。

  • 在某些情况下,合同可以代替例外:

然而,合同可用于嵌入式系统或其他无法承受异常的资源受限系统是一个关键的设计标准。

在前置条件合同中仍然可以使用例外,因为不保证前置条件合同失败后的进一步行为。

  • 可以通过在以下情况下打开/关闭合同检查来减少开销:全部使用、不使用、仅前置条件、仅后置条件。开启合约肯定会增加一些开销,就像任何类型的检查一样。

一些用例(我可以假设,虽然我还没有接近开发合同设计)

  1. 合同 - 在通常情况下,assert()合同更具可读性并且可以在编译时进行优化。
  2. 断言 - 在单元测试、测试框架等中。
  3. 例外情况 - 可以与文章中提到的前置合同一起使用:

在函数体中的任何其他语句之前评估操作的前置条件。如果结果为真,则正常的执行控制继续到函数体中的第一条语句。否则,无法保证进一步执行:程序中止,或抛出异常,或者如果允许继续,则行为未定义。

还有一些关于合同执行的 其他建议,所以我们的调查还为时过早。

于 2015-07-09T12:35:08.353 回答
4

回答您的问题并不容易:这取决于。这是因为目前尚不清楚确切的合同是什么。现在有几个提议和想法:

  • n4378 拉科斯等人。基本上建议标准化一个复杂的断言工具包。在函数实现内部检查合同,提供 3 个不同的断言级别来控制运行时检查的数量,并且可以自定义对断言违规的处理。

  • n4415 dos Reis 等人。n4435 Brown非常相似,并提出了一种基于属性的语法来定义函数接口中的前置条件和后置条件。他们没有详细说明他们对运行时检查和违规行为的控制程度。

最近关于这个主题的论文也较少。有许多细节尚未确定,并且此功能涉及许多不同的领域(例如模块、优化、构建/链接),其中一些标准几乎无法控制。

您关于异常的问题特别困难,因为合同违规处理和异常之间的交互不清楚(例如,合同违规处理程序是否可以抛出(在测试框架中有用)?如果函数是noexcept(true)什么?)。

于 2015-07-09T13:07:23.360 回答