所以我的问题是——如果从析构函数中抛出导致未定义的行为,你如何处理析构函数期间发生的错误?
主要问题是:你不能不失败。毕竟,失败意味着什么?如果向数据库提交事务失败,并且失败(回滚失败),我们的数据完整性会发生什么?
由于对正常和异常(失败)路径都调用了析构函数,因此它们本身不会失败,否则我们将“失败”。
这是一个概念上困难的问题,但解决方案通常是找到一种方法来确保失败不会失败。例如,数据库可能会在提交到外部数据结构或文件之前写入更改。如果事务失败,则可以丢弃文件/数据结构。然后它必须确保从该外部结构/文件提交更改是一个不会失败的原子事务。
务实的解决方案也许只是确保失败失败的可能性在天文数字上是不可能的,因为在某些情况下,让事情不可能失败几乎是不可能的。
对我来说最合适的解决方案是以一种清理逻辑不会失败的方式编写非清理逻辑。例如,如果您想创建一个新的数据结构来清理现有的数据结构,那么您可能会寻求提前创建该辅助结构,以便我们不再需要在析构函数中创建它。
诚然,这说起来容易做起来难,但这是我看到的唯一真正正确的方法。有时我认为应该有能力为正常的执行路径和异常的执行路径编写单独的析构函数逻辑,因为有时析构函数感觉有点像他们通过尝试处理两者来承担双重责任(一个例子是需要明确解除的范围保护; 如果他们可以区分异常破坏路径和非异常破坏路径,他们就不需要这样做)。
最终的问题仍然是我们不能失败,这是一个很难在所有情况下都完美解决的概念设计问题。如果你不被复杂的控制结构所包围,大量的小对象相互交互,它会变得更容易,而是以稍微笨重的方式对你的设计进行建模(例如:带有析构函数的粒子系统来破坏整个粒子系统,而不是每个粒子单独的非平凡析构函数)。当您在这种较粗略的级别上对设计进行建模时,您需要处理的重要析构函数就会减少,并且通常还可以承担确保析构函数不会失败所需的任何内存/处理开销。
最简单的解决方案之一自然是减少使用析构函数。在上面的粒子示例中,也许在破坏/移除粒子时,应该做一些可能由于任何原因而失败的事情。在这种情况下,不是通过可以在异常路径中执行的粒子的 dtor 调用此类逻辑,而是可以在粒子系统移除粒子时全部由粒子系统完成。移除粒子可能总是在非异常路径期间完成。如果系统被破坏,也许它可以只清除所有粒子,而不用打扰可能失败的单个粒子删除逻辑,而可能失败的逻辑仅在粒子系统正常执行期间删除一个或多个粒子时执行。
如果您避免使用非平凡的析构函数处理大量小对象,通常会出现类似的解决方案。你可能会陷入一团乱麻,似乎几乎不可能实现异常安全的地方是,当你确实陷入了许多都具有非平凡 dtors 的小对象中时。
如果任何指定它的东西(包括应该继承其基类的 noexcept 规范的虚函数)试图调用任何可能抛出的东西,如果 nothrow/noexcept 实际转换为编译器错误,这将有很大帮助。这样,如果我们实际上无意中编写了一个可能抛出的析构函数,我们就能够在编译时捕获所有这些东西。