23

今天我了解到swap在 C++ 中不允许抛出异常。

我也知道以下也不能抛出异常:

  • 析构函数
  • 读/写原始类型

还有其他人吗?
或者,是否有某种列表提到了所有可能不会抛出的东西?
(显然,比标准本身更简洁。)

4

3 回答 3

17

不能不应该有很大的区别。对原始类型的操作不能像许多函数和成员函数一样抛出,包括标准库和/或许多其他库中的许多操作。

现在在should not上,您可以包含析构函数和交换。根据您实现它们的方式,它们实际上可以抛出,但您应该避免使用抛出的析构函数,并且在 的情况下swap,提供具有不抛出保证的交换操作是在您的类中实现强异常保证的最简单方法,因为你可以复制一边,对副本执行操作,然后与原件交换。

但请注意,该语言允许析构函数和swap抛出。swap 可以抛出,在最简单的情况下,如果你不重载它,然后std::swap执行复制构造、赋值和销毁,三个操作都可以抛出异常(取决于你的类型)。

析构函数的规则在 C++11 中发生了变化,这意味着没有异常规范的析构函数具有隐式noexcept规范,这反过来意味着如果它抛出异常,运行时将调用terminate,但是您可以将异常规范更改为noexcept(false)然后析构函数也可以抛出。

归根结底,如果不了解代码库,就无法提供异常保证,因为几乎 C++ 中的每个函数都允许抛出异常。

于 2012-07-23T04:03:03.483 回答
9

所以这并不能完美地回答你的问题——出于我自己的好奇心,我进行了一些搜索——但我相信 nothrow保证函数/运算符主要来自 C++ 中可用的任何 C 样式函数以及一些函数任意简单到足以给出这样的保证。一般来说,不期望 C++ 程序提供这种保证(什么时候应该使用 std::nothrow?) 并且甚至不清楚这样的保证是否会在经常使用异常的代码中为您购买任何有用的东西。除了交换、析构函数和原始操作的列表之外,我找不到作为非抛出函数的所有 C++ 函数的完整列表(如果我错过了规定这一点的标准,请纠正我)。此外,在库中未完全定义的函数要求用户实现 nothrows 函数似乎相当罕见。

因此,也许要找到问题的根源,您应该主要假设任何东西都可以在 C++ 中抛出,并在您发现绝对不能抛出异常的东西时将其视为一种简化。编写异常安全代码很像编写无错误代码——它比听起来更难,而且老实说,通常不值得付出努力。此外,异常不安全代码和强大的 nothrow 函数之间存在许多级别。看到这个关于编写异常安全代码的很棒的答案作为对这些点的验证:你(真的)写异常安全代码吗?. 在 boost 站点http://www.boost.org/community/exception_safety.html上有更多关于异常安全的信息。

对于代码开发,我从教授和编码专家那里听到了关于什么应该和不应该抛出异常以及这些代码应该提供什么保证的不同意见。但是一个相当一致的断言是,很容易抛出异常的代码应该非常清楚地记录在案,或者在函数定义中指出抛出的能力(并不总是仅适用于 C++)。可能抛出异常的函数比从不抛出的函数更常见,知道可能发生的异常非常重要。但是保证一个将一个输入除以另一个输入的函数永远不会引发除以 0 异常可能是非常不必要/不需要的。因此 nothrow 可以让人放心,但对于安全代码执行来说不是必需的或总是有用的。

回应对原始问题的评论:

人们有时会说抛出异常的构造函数在容器中或一般情况下是邪恶的,应该始终使用两步初始化和 is_valid 检查。但是,如果构造函数失败,它通常无法修复或处于独特的错误状态,否则构造函数会首先解决问题。检查对象是否有效就像在初始化代码周围放置一个 try catch 块一样困难,您知道这些对象很有可能引发异常。那么哪个是正确的?通常是在代码库的其余部分中使用的那个,或者你的个人喜好。我更喜欢基于异常的代码,因为它让我感觉更灵活,无需检查每个对象的有效性(其他人可能不同意)。

这会给您留下原始问题和评论中列出的扩展名吗?好吧,从提供的来源和我自己的经验来看,从 C++ 的“异常安全”角度担心 nothrow 函数通常是处理代码开发的错误方法。相反,请记住您知道的功能可能是合理的抛出异常并适当地处理这些情况。这通常涉及 IO 操作,您无法完全控制触发异常的原因。如果您遇到了您从未预料到或认为不可能的异常,那么您的逻辑(或您对函数使用的假设)中存在错误,您需要修复源代码以适应。试图保证非平凡的代码(有时甚至是这样)就像说服务器永远不会崩溃——它可能非常稳定,但你可能不会 100% 确定。

于 2012-07-23T04:40:56.290 回答
6

如果您想要这个问题的详尽答案,请访问http://exceptionsafecode.com/并观看仅涵盖 C++03 的 85 分钟视频或涵盖两者的三小时(分两部分)视频C++03 和 C++11。

在编写异常安全代码时,我们假设所有函数都抛出异常,除非我们知道不同。

简而言之,

*) 基本类型(包括数组和指向的指针)可以分配给和从不涉及用户定义运算符的操作(例如仅使用基本整数和浮点值的数学)中使用。请注意,除以零(或任何其结果未在数学上定义的表达式)是未定义的行为,可能会或可能不会抛出,具体取决于实现。

*) 析构函数:发出异常的析构函数在概念上没有错,标准也没有禁止它们。但是,良好的编码指南通常会禁止它们,因为该语言不能很好地支持这种情况。(例如,如果 STL 容器中对象的析构函数抛出,则行为未定义。)

*) 使用 swap() 是提供强异常保证的重要技术,但前提是 swap() 不抛出异常。一般来说,我们不能假设 swap() 是非抛出的,但该视频介绍了如何在 C++03 和 C++11 中为您的用户定义类型创建非抛出交换。

*) C++11 引入了移动语义和移动操作。在 C++11 中,swap() 是使用移动语义实现的,移动操作的情况与 swap() 的情况类似。我们不能假设移动操作不会抛出,但我们通常可以为我们创建的用户定义类型创建非抛出移动操作(并且它们是为标准库类型提供的)。如果我们在 C++11 中提供非抛出移动操作,我们将免费获得非抛出 swap(),但出于性能目的,我们可以选择以任何方式实现我们自己的 swap()。同样,这是视频中的详细介绍。

*) C++11 引入了 noexcept 运算符和函数装饰器。(Classic C++ 中的“throw ()”规范现在已弃用。)它还提供了函数自省功能,因此可以编写代码以根据是否存在非抛出操作来不同地处理情况。

除了视频之外,exceptionsafecode.com 网站上还有关于需要为 C++11 更新的异常的书籍和文章的参考书目。

于 2012-07-30T00:33:38.277 回答