如何检查和处理违反先决条件的行为?

时间:2019-05-03 15:14:41

标签: c++ software-design code-contracts c++20

C ++ 20围绕合同提供了一些令人惊叹的新功能-对于模板来说,这将使生活变得更好-可以将围绕类型或其他编译时要求的约束纳入模板定义中,并通过适当的诊断来实施编译器。是的!

但是,我非常担心在发生运行时先决条件冲突时无条件终止的情况。

https://en.cppreference.com/w/cpp/language/attributes/contract

  

可以用两个违规延续之一来翻译程序   模式:

     

off(如果未选择继续模式,则为默认值):执行后   违反处理程序完成后,将调用std :: terminate;上:   违反处理程序的执行完成后,执行   正常继续。鼓励实现不提供任何内容   查询,设置或修改构建级别或设置或修改的编程方式   修改违规处理程序。

我编写了广泛的面向用户的软件,该软件将所有异常捕获到一个核心执行循环中,在该循环中记录错误并通知用户失败。

在许多情况下,用户最好保存并退出,但在其他许多情况下,可以通过更改他们正在处理的设计/数据文件中的内容来解决错误。

这就是说-只需更改其设计(例如CAD设计),他们希望执行的操作即可成功。例如。代码执行的公差可能太紧,可能会基于此结果计算结果。更改容差后,只需重新运行该过程即可成功(不再违反基础代码中某个地方令人讨厌的先决条件)。

但是前提条件的推销只是终止而没有能力捕获这样的错误并重试该操作?这听起来像是功能集的严重降级。诚然,在某些领域中确实需要这样做。快速失败,提早失败,并且对于先决条件或后置条件,问题出在编写代码的方式上,用户无法纠正这种情况。

但是...这是一个很大的问题,但是...大多数软件都针对运行时提供的未知数据集执行-声称所有软件都必须终止,并且无法期望用户执行纠正这种情况似乎很奇怪。

Herb Sutter在ACCU上的讨论似乎与前提条件和后置条件的违反只是终止条件的观点紧密相关:

https://www.youtube.com/watch?v=os7cqJ5qlzo

我正在寻找其他C ++专业人员根据您的编码经验告诉您的想法吗?

我知道许多项目都不允许例外。如果您正在从事一个这样的项目,这是否意味着您编写代码以在发生无效输入时简单地终止?还是您使用错误状态回退到某些能够以某种方式继续的父代码点?

也许更确切地说-也许我误会了C ++ 20运行时合同的意图本质?

请保持这种文明的态度-如果您的建议是关闭此主题-也许您会很乐意指向一个更合适的论坛进行讨论?

通常,我会尽力让我满意:

如何检查和处理违反先决条件的行为(使用最佳实践)?

1 个答案:

答案 0 :(得分:13)

这实际上归结为这个问题:当您说“前提”一词时,您意味着什么?

您似乎使用该词的方式是指“调用此函数时会被检查的事物”。 Herb,C ++标准以及C ++合约系统的意思是“对于有效执行此功能,它必须是正确的;如果不正确,则您做错了,并且世界是破碎的。”

这种观点实际上可以归结为“合同”的含义。考虑vector::operator[]vector::at()at在C ++标准中没有前提条件合同;如果索引超出范围,则抛出该异常。也就是说,它是at的接口的部分,您可以传递超出范围的值,并且它将以预期的可预测的方式响应。

operator[]并非如此。您可以将超出范围的索引传递给该函数的接口的 not 部分。因此,它具有前提条件约定,即索引不会超出范围。如果将其传递给超出范围的索引,则会得到未定义的行为

所以,让我们看一些简单的例子。我将构建一个vector,然后从用户那里读取一个整数,然后使用该整数来访问以三种不同方式构建的vector

int main()
{
    std::vector<int> ivec = {10, 209, 184, 96};

    int ix;
    std::cin >> ix;

    //case 1:
    try
    {
        std::cout << ivec.at(ix);
    }
    catch(const std::exception &)
    {
        std::cout << "Invalid input!\n";
    }

    //case 2:
    if(0 <= ix && ix < ivec.size())
        std::cout << ivec[ix];
    else
        std::cout << "Invalid Input!\n";

    //case 3:
    std::cout << ivec[ix];

    return 0;
}

在情况1中,我们看到使用at。在输入错误的情况下,我们会捕获异常并对其进行处理。

在情况2中,我们看到使用operator[]。我们检查输入是否在有效范围内,如果是,则调用operator[]

在情况3中,我们看到... 我们代码中的错误。为什么?因为没有人对输入进行消毒。有人必须这样做,operator[]的前提条件是这样做是呼叫者的工作。调用者无法清理其输入,因此代表了损坏的代码。

这就是建立合同的意思:如果代码违反了合同,那是代码违反合同的错误。

但是正如我们所看到的,契约似乎是函数接口的基本组成部分。如果是这样,为什么接口的这一部分位于标准的文本中,而不是位于人们可以看到的函数的可见声明中?右边就是合同语言功能的整个点:允许用户在语言中表达这种特定的东西。

总而言之,合同就是假设,一段代码是关于世界状况的假设。如果该假设不正确,则表示该状态不应该存在,因此您的程序中存在错误。这就是合同语言功能设计的基础。如果您的代码对其进行了测试,那么您就不必假设它了,并且不应使用先决条件对其进行定义。

如果是错误情况,则应使用首选的错误机制,而不是合同。