我知道DbC要求调用方负责前置条件(参数或成员变量的值),我刚刚在其中一本书中读到,实际上很少有人敢把所有责任都留给调用代码,而不检查被调用例程中的输入
但我在想,这不是也会导致重复吗?如果我需要从几个地方调用一个方法怎么办。。在所有这些地方,我都需要确保先决条件得到满足。。
bool AddEmployee(Employee e)
{
//precondition: List of employees is not full, employee is not empty...
EmployeeList.Add(e);
}
我可以从几个模块(员工管理、人力资源模块)中调用它,所以我不知道我是否真的应该检查所有这些地方的先决条件。
合同规定,呼叫者有责任确保满足先决条件。
合同明确规定了谁应对错误负责。如果你不能满足一个先决条件,那就是调用者。如果你不能满足后置条件,那就是被调用者。仅凭这一点就足够有用,值得记录合同。
有时,您可以编写代码,这样就不需要检查前置条件。例如:
Foo()
{
int x = 1;
Bar(x);
}
Bar(int x) [[expects: x>0]]
{
}
您设置x,这样您就知道它不能小于零。
其他时候,您需要执行检查它们。它有时确实会造成重复。我并不经常发现这是一个重大问题,但有时你可能会看到这样的模式:
SafeBar(int x)
{
if (x <= 0) throw SomeException();
else Bar(x);
}
当然,这是假设每次使用都可以以相同的方式处理错误,但情况并非总是如此。
取消先决条件检查是一种性能优化。正如我们所知,过早的优化是万恶之源,所以只有在必要的时候才应该这样做。
现在另一个因素是实现。很少有语言支持在编译时检查约定。它最近被投票加入了C++20,但在撰写本文时只有一个实验性的实现。C++20使用如上所述的属性。属性不应该更改运行时行为。
如果您没有编译时支持,您通常会发现使用某种断言宏的实现。就我个人而言,我使用的是抛出异常的。然后,您使用标准的异常处理机制来处理错误(有些人认为这不合适),但您不一定需要在调用站点检查契约。
为什么这可能不合适?值得记住的是,违反合同是一个错误。如果在不满足先决条件的情况下运行函数,则调用未定义的行为。原则上,任何事情都有可能发生。它甚至可以格式化你的硬盘(尽管这不太可能)。在运行时检查前置条件就像防御编码一样。如果断言导致异常,则永远不会发生未定义的行为。这样更安全,也更容易调试。但从一个角度来看,你修改了合同。
通常,在编译时检查契约是不可决定的。引用相关答案:
如果定理证明者可以证明合同总是违反了,这是一个编译错误。如果定理证明者能够证明合同永远不会被违反,这是一种优化。
证明合同一般等同于解决停工问题因此是不可能的。因此,在很多情况下,定理谚语既不能证明也不能推翻合同。在这种情况下,将发出运行时检查
这个问题被标记为语言不可知,但我对C++20提案的一个问题是,它似乎省略了对其他情况的运行时检查。它还明确表示,在运行时设置违规处理程序是不可能的:
不应该有设置或修改违规处理程序的编程方式
它还强制默认选择在违反合同时调用std::terminate()来结束整个过程。对于像多线程容错任务调度程序这样的东西来说,这将是一件坏事(tm)。一个任务中的错误不应该扼杀整个过程。
我认为理由是C++20合约只是作为一个编译时特性。这包括在编译时使用constexpr和consteval对它们进行评估。该功能允许编译器供应商开始添加定理证明器来检查契约,这在以前是不可能的。这很重要,并带来了许多新的机会。
希望接下来会有一个考虑到运行时可能性的实用修改。不利的一面是,在短期内,你需要保留你的断言。如果像我一样,您使用Doxygen进行文档(它还不理解合同),那么您就有三重冗余。例如:
///
/// @brief
/// Bar does stuff with x
///
/// @pre
/// @code x > 0 @endcode
///
void Bar(int x) [[expects: x > 0]]
{
{ //pre-conditions
assertion(x>0);
}
...do stuff
}
请注意,Cassert()宏不会抛出。因此,我们使用了自己的assetion()宏。CppCoreGuidelines支持库包括Expects()和Ensures()。我不确定他们是抛出还是调用std::terminate()。