C++ 如何检查和处理前提条件冲突?

C++ 如何检查和处理前提条件冲突?,c++,software-design,code-contracts,c++20,C++,Software Design,Code Contracts,C++20,C++20围绕契约提供了一些令人惊叹的新特性——对于模板来说,这将使生活更加美好——其中类型或其他编译时需求的约束可以烘焙到模板定义中,并由编译器通过适当的诊断来实施。耶 然而,我非常担心当运行时先决条件发生冲突时,无条件终止的趋势 一个程序可以用两种语言中的一种进行翻译 模式: 如果未选择继续模式,则关闭默认值:执行后 当违规处理程序完成时,调用std::terminate;关于: 冲突处理程序的执行完成后,执行 正常继续。鼓励实现不提供任何 以编程方式查询、设置或修改生成级别或设置或修改 修

C++20围绕契约提供了一些令人惊叹的新特性——对于模板来说,这将使生活更加美好——其中类型或其他编译时需求的约束可以烘焙到模板定义中,并由编译器通过适当的诊断来实施。耶

然而,我非常担心当运行时先决条件发生冲突时,无条件终止的趋势

一个程序可以用两种语言中的一种进行翻译 模式:

如果未选择继续模式,则关闭默认值:执行后 当违规处理程序完成时,调用std::terminate;关于: 冲突处理程序的执行完成后,执行 正常继续。鼓励实现不提供任何 以编程方式查询、设置或修改生成级别或设置或修改 修改违规处理程序

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

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

这就是说,只要改变他们的设计,例如CAD设计,他们希望执行的操作现在就会成功。例如,代码执行时的容差可能太小,这可能导致基于该容差计算结果。只要在更改容差后重新运行该过程,就会成功,而不会再违反底层代码中某个有问题的先决条件

但是推动先决条件只是终止,没有能力捕获这样的错误并重试操作?对我来说,这听起来像是功能集的严重退化。诚然,在某些领域,这是完全可取的。Fail fast、Fail early,对于先决条件或后决条件,问题在于编写代码的方式,用户无法纠正这种情况

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

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

我在寻找其他C++程序员从你的经验编码中得到什么信息? 我知道很多项目不允许例外。如果您正在处理这样一个项目,这是否意味着您编写的代码只是在出现无效输入时终止?或者您是否使用错误状态返回到某个能够以某种方式继续的父代码点

也许更重要的是——也许我误解了C++20运行时契约的意图的本质

请保持文明——如果你的建议是结束这一讨论——也许你可以指出一个更合适的论坛来进行讨论

一般来说,我会尽力回答,让我满意:


如何使用最佳实践检查和处理前提条件冲突?

问题的关键在于:你说的前提条件这个词是什么意思

您使用这个词的方式似乎是指当您调用这个函数时被检查的东西。HEPER、C++标准和C++契约系统意味着它是一个必须正确执行的函数,如果不是真的,那么你做了一个错误的事情,世界就被破坏了。 这种观点实际上可以归结为合同的含义。考虑向量::运算符[]与向量::AT。在C++标准中没有先决条件合同;如果索引超出范围,它将抛出。也就是说,它是at接口的一部分,您可以传递超出范围的值,它将以预期的、可预测的方式响应

操作员[]的情况并非如此。它不是该函数接口的一部分,您可以将其传递到范围之外的索引。因此,它有一个先决条件,即指数不超出范围。如果您向它传递一个超出范围的索引,则会得到未定义的行为

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

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中,我们看到运算符[]的使用。我们检查输入是否在有效范围内,如果在有效范围内,请呼叫操作员[]

在案例3中,我们看到。。。我们的代码中有一个bug。为什么?因为没有人对输入进行消毒。某人 ad to和操作员[]的前提条件是呼叫方的工作就是这样做。调用方未能清理其输入,因此表示代码已损坏

这就是建立合同的意义:如果代码破坏了合同,那么破坏合同是代码的错

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

总而言之,合同是一段代码对世界状态做出的假设。如果这个假设是不正确的,那么它代表了一个不应该存在的世界状态,因此您的程序有一个bug。这就是契约语言特性设计的基本思想。如果您的代码测试它,那么它不是您假设的东西,您不应该使用先决条件来定义它


如果是一种错误情况,那么你应该使用你喜欢的错误机制,而不是合同。

归根结底,问题是:你说“前提条件”这个词是什么意思

您使用这个词的方式似乎是指当您调用这个函数时被检查的东西。HEPER、C++标准和C++契约系统意味着它是一个必须正确执行的函数,如果不是真的,那么你做了一个错误的事情,世界就被破坏了。 这种观点实际上可以归结为合同的含义。考虑向量::运算符[]与向量::AT。在C++标准中没有先决条件合同;如果索引超出范围,它将抛出。也就是说,它是at接口的一部分,您可以传递超出范围的值,它将以预期的、可预测的方式响应

操作员[]的情况并非如此。它不是该函数接口的一部分,您可以将其传递到范围之外的索引。因此,它有一个先决条件,即指数不超出范围。如果您向它传递一个超出范围的索引,则会得到未定义的行为

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

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中,我们看到运算符[]的使用。我们检查输入是否在有效范围内,如果在有效范围内,请呼叫操作员[]

在案例3中,我们看到。。。我们的代码中有一个bug。为什么?因为没有人对输入进行消毒。有人不得不这么做,接线员[]的先决条件说这是打电话的人的工作。调用方未能清理其输入,因此表示代码已损坏

这就是建立合同的意义:如果代码破坏了合同,那么破坏合同是代码的错

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

总而言之,合同是一段代码对世界状态做出的假设。如果这个假设是不正确的,那么它代表了一个不应该存在的世界状态,因此您的程序有一个bug。这就是契约语言特性设计的基本思想。如果您的代码测试它,那么它不是您假设的东西,您不应该使用先决条件来定义它


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

确定。你对一个我觉得太宽泛的问题找到了一个很好的答案+1.我撤回了我的投票,删除了我以前的评论。“说得好,”尼科尔,这是个微妙的问题。我很欣赏您将定义一个接口(比如oor冲突)与一个需要净化输入的接口(在编译器可访问的契约声明中明确指出)区别开来。非常感谢。我可能会投票给这个答案,但我会先看看其他人怎么说。非常感谢。@Mordachai:让合同成为语言特性的目标之一是确保人们理解这不是一个微妙的问题。编写代码时所依据的假设、哪些是假设契约、哪些是预期错误是用户在设计界面时应该考虑的一件非常重要的事情。@Nicolabolas新契约语言功能并不微妙,向调用者表达东西是一个非常好的意图,值得称赞。事实上
e只是表示世界破裂的能力,而不是显式的,如果输入为负,我们无法计算值,在我看来,这是非常缺乏的。我想要两者——表达这些先决条件的能力必须得到满足,否则宇宙就会内爆——而这些条件必须得到满足,否则我们就会出错。好的。你对一个我觉得太宽泛的问题找到了一个很好的答案+1.我撤回了我的投票,删除了我以前的评论。“说得好,”尼科尔,这是个微妙的问题。我很欣赏您将定义一个接口(比如oor冲突)与一个需要净化输入的接口(在编译器可访问的契约声明中明确指出)区别开来。非常感谢。我可能会投票给这个答案,但我会先看看其他人怎么说。非常感谢。@Mordachai:让合同成为语言特性的目标之一是确保人们理解这不是一个微妙的问题。编写代码时所依据的假设、哪些是假设契约、哪些是预期错误是用户在设计界面时应该考虑的一件非常重要的事情。@Nicolabolas新契约语言功能并不微妙,向调用者表达东西是一个非常好的意图,值得称赞。事实上,只有表达世界的能力被打破了,而不是明确的,如果输入是负数,我们无法计算一个值,在我看来,这是非常缺乏的。我想要两者——必须满足表达这些先决条件的能力,否则宇宙就会内爆——并且必须满足这些条件,否则就会导致错误。违反合同就是一个bug。尽管这篇博文是关于Midori的,但它也适用于C++20合同。q、 错误是不可恢复的错误!您担心的是将可恢复错误与bug混为一谈。这些都是独立的事情,通常是错误的,我认为可以用同样的方式来处理。违反合同就是一个bug。尽管这篇博文是关于Midori的,但它也适用于C++20合同。q、 错误是不可恢复的错误!您担心的是将可恢复错误与bug混为一谈。这些都是独立的东西,通常是错误的,我认为可以用同样的方法来治疗。