以移出状态访问对象

时间:2019-09-23 16:15:41

标签: c++

假设我有一个管理资源的类。每个方法都有一个必须满足的前提:被管理资源必须处于有效状态(与unique_ptr及其operator*operator->相同),并且对象是否位于移出状态,则不满足该前提条件。对于我要访问移动对象的情况(无论出于何种原因意外或故意),都会引发以下问题:

  • 在每种方法开始时检查(断言)前提是否满足,这是一个好的设计吗?
  • 如果是的话,如果不是的话,解决这个问题的好设计到底是如何呢?也许抛出异常或什么? (我了解这是一个基于意见的问题)

4 个答案:

答案 0 :(得分:0)

在C ++中,通常最好使用assert头中的<cassert>来确保满足某些条件,并且在发行版本中的开销为零。我认为在这里扔球并不是一个特别好的主意,但是尽管如此,它还是要权衡取舍。抛出使编写异常安全代码变得更加困难,并且使条件检查成为必需。我会说assert,并记录下来必须满足这些先决条件。例如,std::vector::operator[]在许多实现中都执行此操作,以检查调试模式下的出站访问。

我只想澄清一下,“移出”对象通常应具有有效状态。用户无法对其状态或值做任何假设,但他们应该能够重新分配或重用该对象。

答案 1 :(得分:0)

  

在每个方法开始时检查(断言)前提是否满足,这是一个好的设计吗?

如果可以对其进行静态声明,则可以。在运行期间考虑具有这样的代码的呼叫者

for (int i=0; i<100000;++i) {
    foo.bar();
}

在条件不变的情况下,每次迭代都检查前提条件会浪费资源。

  

如果是的话,如果不是的话,解决这个问题的好设计到底是如何呢?也许抛出异常或什么? (我了解这是一个基于意见的问题)

这取决于您希望对呼叫者有多好。一种有效的选择是清楚地说明先决条件,如果呼叫者违反了这些先决条件,那么前提条件就是它们自己。比较一下std::vector::operator[]:您想传递越界索引吗?没问题(从操作员的角度来看),未定义的行为就是您所得到的(从您的角度来看确实是个问题)。您不能保证在所有情况下都有明确的行为,尤其是在呼叫者不在乎您的前提条件的情况下。在某些情况下,您可能会引发异常,但这取决于细节。

答案 2 :(得分:0)

  

在每个方法开始时检查(断言)前提是否满足,这是一个好的设计吗?

就像编程中的大多数事情一样,这是一个折衷。什么才是好事取决于您是否更珍惜获得的收益。

您在交易中损失的是性能:不执行检查至少要快,并且通常比执行检查要快。这种效率损失是否显着取决于使用情况。在CPU限制算法的热路径内:可能是。在处理网络通信的代码中:可能不是。要找出差异是否显着,您需要进行测量。

  

如果不是这样,解决这个问题的好的设计到底有多好?

有几种方法可以在C ++中传达错误:

  • 最简单的形式是中止程序。优点是不会出现未知的副作用,例如由于不确定的行为而导致的安全漏洞。当程序无法恢复时,这是一个不错的选择……但是作为类的实现者,您如何知道类的用户是否可以恢复?

  • 标准assert宏属于终止程序的类别,但是通常仅在测试中启用检查,而在发布模式下禁用检查,这可能会使程序容易受到攻击。

  • 使用异常是一种方便的方法,可以让客户端代码选择是要从客户端代码中恢复(try-catch)还是让程序安全地终止。违反条件之前/之后通常是例外,在这种情况下,适当的例外是合适的。

  • 使用传统的C样式错误代码。这将迫使客户端编写自己的错误处理代码,以避免潜在的用户负担。

每种方式各有优点和缺点。您应该根据需要选择。

答案 3 :(得分:0)

我还将使用类似断言的解决方案来检查该代码中的前提条件。但是,我将使用一些特定于编译器的魔术来优化可能的/不太可能的情况,而不是使用裸露的assert,就像在Microsoft's GSL library中所做的那样。

您有GSL_LIKELYGSL_UNLIKELYGSL_ASSUME宏以及ExpectsEnsures构造。它们优于普通的assert,因为基于它们,编译器可以针对可能/不太可能的分支执行更好的代码优化和配置文件汇编。

此外,非常酷的事情是,您可以调整GSL的配置,然后决定在失败的情况下如何处理:抛出,终止或什么都不做。这样,您可以轻松测试不同的行为,而无需修改整个代码库。您只需要更改一些编译定义即可。

还有另一件事:使用它们更为简洁。我认为Expects(m_data != nullptr)assert(m_data != nullptr)更具可读性。