毁灭性的例外

再一次说明为什么在析构函数中抛出异常不好


许多C ++专家(例如Herb Sutter )告诉我们,在析构函数中抛出异常是不好的,因为您可以在堆栈升级期间已经抛出异常的情况下进入析构函数,并且如果此时抛出另一个异常,则将调用std作为结果。 ::终止() 。 关于此主题的C ++ 17语言标准(以下称为N4713草案的免费版本)告诉我们以下内容:


18.5.1 std :: terminate()函数[except.terminate]

1在某些情况下,必须放弃异常处理,以减少不太细微的错误处理技术。 [注意:

这些情况是:

...

(1.4)当堆栈展开(18.2)期间对象的销毁因引发异常而终止时,或

...

-尾注]

让我们来看一个简单的例子:


#include <iostream> class PrintInDestructor { public: ~PrintInDestructor() noexcept { std::cerr << "~PrintInDestructor() invoked\n"; } }; void throw_int_func() { std::cerr << "throw_int_func() invoked\n"; throw 1; } class ThrowInDestructor { public: ~ThrowInDestructor() noexcept(false) { std::cerr << "~ThrowInDestructor() invoked\n"; throw_int_func(); } private: PrintInDestructor member_; }; int main(int, char**) { try { ThrowInDestructor bad; throw "BANG!"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } catch (const char* c) { std::cerr << "Catched const char* exception: " << c << "\n"; } catch (...) { std::cerr << "Catched unknown exception\n"; } return 0; } 

结果:


 ~ThrowInDestructor() invoked throw_int_func() invoked ~PrintInDestructor() invoked terminate called after throwing an instance of 'int' Aborted 

注意PrintInDestructor析构函数仍被调用,即 抛出第二个异常后,堆栈升级不会中断。 关于此主题的标准(相同的段落18.5.1)规定:


2 ...在没有找到匹配处理程序的情况下,
在调用std :: terminate()之前是否取消堆栈的堆栈是由实现定义的。 在
搜索处理程序(18.3)遇到函数的最外层块的情况
非抛出异常规范(18.4),由实现定义,堆栈是否解绕,
在调用std :: terminate()之前部分解开或根本不解开...

我在GCC8.2、7.3 )和Clang6.0、5.0 )的多个版本上测试了此示例,到处都在推广堆栈。 如果遇到实现定义不同的编译器,请在注释中写出。


还应注意,仅当从析构函数抛出异常时才取消堆栈堆栈时,才调用std :: terminate() 。 如果析构函数内部有一个try / catch块可以捕获异常并且不会进一步抛出异常,则这不会中断外部异常堆栈的提升。


 class ThrowCatchInDestructor { public: ~ThrowCatchInDestructor() noexcept(false) { try { throw_int_func(); } catch (int i) { std::cerr << "Catched int in ~ThrowCatchInDestructor(): " << i << "\n"; } } private: PrintInDestructor member_; }; int main(int, char**) { try { ThrowCatchInDestructor good; std::cerr << "ThrowCatchInDestructor instance created\n"; throw "BANG!"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } catch (const char* s) { std::cerr << "Catched const char* exception: " << s << "\n"; } catch (...) { std::cerr << "Catched unknown exception\n"; } return 0; } 

显示


 ThrowCatchInDestructor instance created throw_int_func() invoked Catched int in ~ThrowCatchInDestructor(): 1 ~PrintInDestructor() invoked Catched const char* exception: BANG! 

如何避免不愉快的情况? 从理论上讲,一切都很简单:永远不要在析构函数中抛出异常。 然而,实际上,精美而优雅地实现这一简单要求并不是那么简单。


如果不能,但真的想...


我将立即注意到,我并不是想证明从析构函数引发异常的理由,并且紧跟Sutter,Meyers和其他C ++专家,我敦促您不要尝试这样做(至少在新代码中)。 但是,在实际操作中,程序员很可能会遇到遗留代码,这很难导致高标准。 另外,下面经常描述的技术在调试过程中会派上用场。

例如,我们正在开发一个带有包装器类的库,该包装器用某种资源封装工作。 根据RAII的原则,我们在构造函数中获取资源,并且必须在析构函数中释放它。 但是,如果释放资源的尝试失败了怎么办? 解决此问题的选项:


  • 忽略该错误。 不好,因为我们隐藏了可能影响系统其他部分的问题。
  • 写入日志。 总比忽略它更好,但仍然很糟糕,因为 我们的图书馆对使用它的系统中采用的日志记录政策一无所知。 可以将标准日志重定向到/ dev / null,因此,再次,我们将不会看到错误。
  • 将资源的释放带到一个单独的函数中,该函数返回一个值或引发异常,并迫使类用户自行调用它。 这很糟糕,因为用户可能会完全忘记这样做,并且我们将收到资源泄漏。
  • 引发异常。 在通常情况下是好的,因为 类用户可以捕获异常并以标准方式获取有关错误的信息。 升级堆栈时不好,因为 导致std :: terminate()

如何理解我们当前是否正在异常推进堆栈? 在C ++中,有一个特殊的函数std :: uncaught_exception() 。 有了它的帮助,我们可以在正常情况下安全地引发异常,或者做一些不太正确的事情,但不会导致堆栈提升期间引发异常。


 class ThrowInDestructor { public: ~ThrowInDestructor() noexcept(false) { if (std::uncaught_exception()) { std::cerr << "~ThrowInDestructor() stack unwinding, not throwing\n"; } else { std::cerr << "~ThrowInDestructor() normal case, throwing\n"; throw_int_func(); } } private: PrintInDestructor member_; }; int main(int, char**) { try { ThrowInDestructor normal; std::cerr << "ThrowInDestructor normal destruction\n"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } try { ThrowInDestructor stack_unwind; std::cerr << "ThrowInDestructor stack unwinding\n"; throw "BANG!"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } catch (const char* s) { std::cerr << "Catched const char* exception: " << s << "\n"; } catch (...) { std::cerr << "Catched unknown exception\n"; } return 0; } 

结果:


 ThrowInDestructor normal destruction ~ThrowInDestructor() normal case, throwing throw_int_func() invoked ~PrintInDestructor() invoked Catched int exception: 1 ThrowInDestructor stack unwinding ~ThrowInDestructor() stack unwinding, not throwing ~PrintInDestructor() invoked Catched const char* exception: BANG! 

请注意,自C ++ Standard 17起, 不推荐使用 std :: uncaught_exception()函数,因此,要编译该示例,必须禁止相应的编译(请参见带有示例示例存储库 )。


这个函数的问题是它检查我们是否正在异常旋转堆栈的过程中。 但是无法理解在堆栈升级过程中是否调用了当前析构函数。 结果,如果有堆栈提升,但是通常调用某些对象的析构函数,则std :: uncaught_exception()仍将返回true


 class MayThrowInDestructor { public: ~MayThrowInDestructor() noexcept(false) { if (std::uncaught_exception()) { std::cerr << "~MayThrowInDestructor() stack unwinding, not throwing\n"; } else { std::cerr << "~MayThrowInDestructor() normal case, throwing\n"; throw_int_func(); } } }; class ThrowCatchInDestructor { public: ~ThrowCatchInDestructor() noexcept(false) { try { MayThrowInDestructor may_throw; } catch (int i) { std::cerr << "Catched int in ~ThrowCatchInDestructor(): " << i << "\n"; } } private: PrintInDestructor member_; }; int main(int, char**) { try { ThrowCatchInDestructor stack_unwind; std::cerr << "ThrowInDestructor stack unwinding\n"; throw "BANG!"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } catch (const char* s) { std::cerr << "Catched const char* exception: " << s << "\n"; } catch (...) { std::cerr << "Catched unknown exception\n"; } return 0; } 

结果:


 ThrowInDestructor stack unwinding ~MayThrowInDestructor() stack unwinding, not throwing ~PrintInDestructor() invoked Catched const char* exception: BANG! 

在新的C ++ 17标准中,引入了std :: uncaught_exceptions()函数来替换std :: uncaught_exception() (请注意复数),该函数代替布尔值返回当前活动异常的数量(此处有详细说明)。


这就是通过std :: uncaught_exceptions()解决上述问题的方法:


 class MayThrowInDestructor { public: MayThrowInDestructor() : exceptions_(std::uncaught_exceptions()) {} ~MayThrowInDestructor() noexcept(false) { if (std::uncaught_exceptions() > exceptions_) { std::cerr << "~MayThrowInDestructor() stack unwinding, not throwing\n"; } else { std::cerr << "~MayThrowInDestructor() normal case, throwing\n"; throw_int_func(); } } private: int exceptions_; }; class ThrowCatchInDestructor { public: ~ThrowCatchInDestructor() noexcept(false) { try { MayThrowInDestructor may_throw; } catch (int i) { std::cerr << "Catched int in ~ThrowCatchInDestructor(): " << i << "\n"; } } private: PrintInDestructor member_; }; int main(int, char**) { try { ThrowCatchInDestructor stack_unwind; std::cerr << "ThrowInDestructor stack unwinding\n"; throw "BANG!"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } catch (const char* s) { std::cerr << "Catched const char* exception: " << s << "\n"; } catch (...) { std::cerr << "Catched unknown exception\n"; } return 0; } 

结果:


 ThrowInDestructor stack unwinding ~MayThrowInDestructor() normal case, throwing throw_int_func() invoked Catched int in ~ThrowCatchInDestructor(): 1 ~PrintInDestructor() invoked Catched const char* exception: BANG! 

当我真的真的想一次抛出一些异常时


std :: uncaught_exceptions()避免调用std ::终止() ,但无助于正确处理多个异常。 理想情况下,我希望有一种机制可以保存所有引发的异常,然后在一个地方处理它们。


我想再次提醒我,我在下面提出的机制仅用于说明该概念,不建议在实际的工业代码中使用。

这个想法的实质是捕获异常并将其保存在容器中,然后一次获取并处理它们。 为了保存异常对象,C ++具有特殊类型std :: exception_ptr 。 标准中的类型结构没有公开,但是可以说每个异常对象实际上是shared_ptr


然后如何处理这些异常? 为此 ,有一个函数std :: rethrow_exception() ,它接受一个指针std :: exception_ptr并引发相应的异常。 我们只需要使用相应的catch部分来捕获它并对其进行处理,之后我们就可以继续下一个异常对象。


 using exceptions_queue = std::stack<std::exception_ptr>; // Get exceptions queue for current thread exceptions_queue& get_queue() { thread_local exceptions_queue queue_; return queue_; } // Invoke functor and save exception in queue void safe_invoke(std::function<void()> f) noexcept { try { f(); } catch (...) { get_queue().push(std::current_exception()); } } class ThrowInDestructor { public: ~ThrowInDestructor() noexcept { std::cerr << "~ThrowInDestructor() invoked\n"; safe_invoke([]() { throw_int_func(); }); } private: PrintInDestructor member_; }; int main(int, char**) { safe_invoke([]() { ThrowInDestructor bad; throw "BANG!"; }); auto& q = get_queue(); while (!q.empty()) { try { std::exception_ptr ex = q.top(); q.pop(); if (ex != nullptr) { std::rethrow_exception(ex); } } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } catch (const char* s) { std::cerr << "Catched const char* exception: " << s << "\n"; } catch (...) { std::cerr << "Catched unknown exception\n"; } } return 0; } 

结果:


 ~ThrowInDestructor() invoked throw_int_func() invoked ~PrintInDestructor() invoked Catched const char* exception: BANG! Catched int exception: 1 

在上面的示例中,堆栈用于保存异常对象,但是,将根据FIFO原理执行异常处理(也就是说,从逻辑上讲,这是队列-首先处理的异常将被首先处理)。


结论


在对象析构函数中引发异常确实不是一个好主意,在任何新代码中,我强烈建议不要通过声明noexcept析构函数来做到这一点。 但是,在遗留代码的支持和调试下,可能需要正确处理从析构函数抛出的异常,包括在堆栈提升期间,而现代C ++为我们提供了这种机制。 我希望本文中提出的想法可以帮助您走上这条艰难的道路。


参考文献


带有本文示例的存储库

Source: https://habr.com/ru/post/zh-CN433944/


All Articles