12

在 C++11 中,没有任何异常规范的析构函数用 隐式声明noexcept,这是对 C++03 的更改。因此,过去在 C++03 中从析构函数中抛出的代码在 C++11 中仍然可以正常编译,但一旦尝试从这样的析构函数中抛出,就会在运行时崩溃。

由于这样的代码没有编译时错误,它如何安全地转换到 C++11,没有将代码库中所有现有的析构函数声明为 is noexcept(false),这将是非常冗长和侵入性的,或者检查每个析构函数是否可能抛出,这将非常耗时且容易出错,或者在运行时捕获并修复所有崩溃,这永远不能保证找到所有此类情况?

4

3 回答 3

9

请注意,规则实际上并不那么残酷。只有隐式声明的析构函数是,析构函数才会是隐noexcept式的。因此,将至少一个基类或成员类型标记为noexcept (false)noexcept破坏整个层次结构/聚合的性质。

#include <type_traits>

struct bad_guy
{
  ~bad_guy() noexcept(false) { throw -1; }
};

static_assert(!std::is_nothrow_destructible<bad_guy>::value,
              "It was declared like that");

struct composing
{
  bad_guy member;
};

static_assert(!std::is_nothrow_destructible<composing>::value,
              "The implicity declared d'tor is not noexcept if a member's"
              " d'tor is not");

struct inheriting : bad_guy
{
  ~inheriting() { }
};

static_assert(!std::is_nothrow_destructible<inheriting>::value,
              "The d'tor is not implicitly noexcept if an implicitly"
              " declared d'tor wouldn't be.  An implicitly declared d'tor"
              " is not noexcept if a base d'tor is not.");

struct problematic
{
  ~problematic() { bad_guy {}; }
};

static_assert(std::is_nothrow_destructible<problematic>::value,
              "This is the (only) case you'll have to look for.");

尽管如此,我同意Chris Beck的观点,即你迟早应该摆脱你的 throwing 析构函数。它们还可以让你的 C++98 程序在最不方便的时候爆炸。

于 2015-10-05T20:07:40.527 回答
5

正如5gon12eder所提到的,有一些规则会导致没有异常规范的析构函数被隐式声明为noexceptor noexcept(false)。如果您的析构函数可能抛出并且您将其留给编译器来决定其异常规范,那么您将在玩轮盘赌,因为您依赖于编译器的决定,该决定受类的祖先和成员以及它们的祖先和成员递归地影响,这太复杂而无法跟踪,并且在代码的演变过程中可能会发生变化。因此,当定义一个带有可能抛出的主体且没有异常规范的析构函数时,它必须显式声明为noexcept(false). 另一方面,如果您确定 body 可能不会抛出,您可能需要声明它noexcept更明确地帮助编译器优化,但如果你选择这样做要小心,因为如果你的类的任何成员/祖先的析构函数决定抛出,你的代码将在运行时中止。

请注意,任何隐式定义的析构函数或具有空主体的析构函数都不会造成任何问题。noexcept只有当所有成员和祖先的所有析构函数也是如此时,它们才是隐式的noexcept

因此,进行转换的最佳方法是找到所有具有非空主体且没有异常规范的析构函数,并声明每个可能抛出的析构函数noexcept(false)。请注意,您只需要检查析构函数的主体 - 它执行的任何立即抛出或由它调用的函数递归完成的任何抛出。无需检查具有空主体的析构函数、具有现有异常规范的析构函数或任何隐式定义的析构函数。在实践中,不会有那么多需要检查的内容,因为这些内容的普遍用途只是释放资源。

由于我是在回答自己,这正是我最终在我的情况下所做的事情,毕竟它并没有那么痛苦。

于 2015-10-05T22:50:01.697 回答
3

我自己也经历过同样的困境。

基本上我得出的结论是,接受那些析构函数正在投掷的事实并忍受其后果通常比让他们不投掷的痛苦要糟糕得多。

原因是当你抛出析构函数时,你会冒更多的不稳定和不可预测的状态。

举个例子,我曾经参与过一个项目,由于各种原因,一些开发人员在项目的某些部分使用异常来进行流控制,并且多年来一直运行良好。后来,有人注意到在项目的不同部分,有时客户端未能发送它应该发送的一些网络消息,因此他们创建了一个 RAII 对象,该对象将在其析构函数中发送消息。有时网络会抛出异常,所以这个 RAII 析构函数会抛出,但谁在乎呢?它没有要清理的内存,因此它不是泄漏。

这在 99% 的情况下都可以正常工作,除非异常流控制路径碰巧穿过网络,然后也会引发异常。然后,你有两个实时异常同时被解除,所以“砰你死了”,用 C++ FAQ 的不朽话语来说。

老实说,我宁愿让程序在析构函数抛出时立即终止,这样我们就可以与编写抛出析构函数的人交谈,而不是尝试使用故意抛出析构函数来维护程序,这似乎是委员会/社区的共识. 所以他们做了这个突破性的改变来帮助你断言你的析构函数是好的而不是抛出。如果您的遗留代码库中有很多 hack,可能需要做很多工作,但是如果您想继续开发和维护它,至少在 C++11 标准上,您最好进行清理工作了析构函数。

底线:

你是对的,你不能真的希望保证你找到所有可能的抛出析构函数的实例。因此,在某些情况下,当您的代码在 C++11 编译时,可能会在不符合 C++98 标准的情况下崩溃。但总的来说,清理析构函数并以 C++11 运行可能会比仅使用旧标准的抛出析构函数稳定得多。

于 2015-10-05T19:34:56.213 回答