dra*_*oot 11 c++ destructor exception-handling exception-specification c++11
在C++ 11中,隐式声明了没有任何异常规范的析构函数 noexcept,这是对C++ 03的更改.因此,用于从C++ 03中的析构函数抛出的代码仍然可以在C++ 11中正常编译,但是一旦尝试从这样的析构函数抛出就会在运行时崩溃.
由于这样的代码没有编译时错误,如何将它安全地转换为C++ 11,而不是将代码库中的所有现有析构函数声明为noexcept(false),这实际上是过于冗长和干扰,或者检查每个析构函数是否有可能抛出,这将非常耗时且容易出错,或者在运行时捕获并修复所有崩溃,这永远无法保证找到所有这些情况?
请注意,规则实际上并不是那么残酷.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.");
Run Code Online (Sandbox Code Playgroud)
尽管如此,我同意Chris Beck的说法,你应该迟早摆脱你的投掷析构函数.它们还可以使您的C++ 98程序在最不方便的时候爆炸.
正如5gon12eder所提到的,有一些规则会导致没有异常规范的析构函数被隐式声明为noexceptor 或noexcept(false)。如果您的析构函数可能抛出异常,而您将其留给编译器来决定其异常规范,那么您将是在玩轮盘赌,因为您依赖于受类的祖先和成员以及它们的祖先和成员递归影响的编译器的决定,这太复杂而无法跟踪,并且在您的代码演变过程中可能会发生变化。因此,在定义具有可能抛出的主体且没有异常说明的析构函数时,必须将其显式声明为noexcept(false). 另一方面,如果您确定主体可能不会抛出,则可能需要声明它noexcept 更明确并帮助编译器优化,但如果您选择这样做,请小心,因为如果您的类的任何成员/祖先的析构函数决定抛出,您的代码将在运行时中止。
请注意,任何隐式定义的析构函数或具有空主体的析构函数都不会造成问题。只有noexcept当所有成员和祖先的所有析构函数都是隐式时,它们才是隐式的noexcept。
因此,进行转换的最佳方法是找到所有具有非空主体且没有异常规范的析构函数,并声明每个可能抛出的析构函数noexcept(false)。请注意,您只需要检查析构函数的主体 - 它执行的任何立即抛出或它调用的函数执行的任何抛出,递归。无需检查具有空主体的析构函数、具有现有异常规范的析构函数或任何隐式定义的析构函数。在实践中,不会有那么多需要检查的,因为它们的普遍用途只是释放资源。
既然我是在回答自己,这正是我最终在我的情况下所做的,毕竟这并没有那么痛苦。
我自己也经历过同样的困境。
基本上我的结论是,接受这些析构函数正在抛出的事实并承受其后果通常比经历让它们不抛出的痛苦要糟糕得多。
原因是,当您抛出析构函数时,您将面临更加不稳定和不可预测的状态的风险。
举个例子,我曾经参与过一个项目,由于各种原因,一些开发人员在项目的某些部分使用异常进行流程控制,并且多年来一直运行良好。后来,有人注意到在项目的不同部分中,有时客户端无法发送一些它应该发送的网络消息,因此他们创建了一个 RAII 对象,该对象将在其析构函数中发送消息。有时网络会抛出异常,因此 RAII 析构函数会抛出异常,但谁在乎呢?它没有内存需要清理,因此不是泄漏。
这在 99% 的情况下都可以正常工作,除非异常流控制路径碰巧穿过网络,然后也会引发异常。然后,您会同时解除两个实时异常,因此,用 C++ FAQ 中不朽的话来说,“砰,您死定了”。
老实说,我宁愿让程序在析构函数抛出时立即终止,这样我们就可以与编写抛出析构函数的人进行交谈,而不是尝试维护一个故意抛出析构函数的程序,这似乎是委员会/社区的共识。因此,他们做出了这一重大更改,以帮助您断言您的析构函数是好的并且不会抛出异常。如果您的遗留代码库中有很多 hack,这可能是一项繁重的工作,但如果您想继续开发它并维护它,至少在 C++11 标准上,您可能最好做清理工作上析构函数。
底线:
你是对的,你不能真正希望保证找到抛出析构函数的所有可能实例。因此,在某些情况下,当您的代码在 C++11 下编译时,如果不符合 C++98 标准,它可能会崩溃。但总的来说,清理析构函数并以 C++11 运行可能比仅使用旧标准的抛出析构函数要稳定得多。