大多数人说永远不要从析构函数抛出异常——这样做会导致未定义的行为。Stroustrup指出,“vector析构函数显式地为每个元素调用析构函数。这意味着如果元素析构函数抛出,则vector销毁失败…实际上没有很好的方法来防止析构函数抛出异常,因此标准库不保证元素析构函数是否抛出”(摘自附录E3.2)。

这篇文章似乎不是这么说的——抛出析构函数或多或少是可以的。

所以我的问题是,如果从析构函数抛出导致未定义的行为,你如何处理析构函数期间发生的错误?

如果在清理操作期间发生错误,您会忽略它吗?如果它是一个可以在堆栈中处理但不能在析构函数中处理的错误,那么从析构函数抛出异常难道没有意义吗?

显然,这种错误很少见,但也有可能发生。


当前回答

我的问题是,如果从析构函数抛出会导致 未定义的行为,如何处理过程中发生的错误 析构函数?

主要的问题是:你不能失败到失败。失败到底意味着什么?如果将事务提交到数据库失败,并且事务未能失败(回滚失败),那么数据的完整性会发生什么变化?

由于析构函数在正常路径和异常(失败)路径上都被调用,它们本身不能失败,否则我们就是“失败”。

这是一个概念上很困难的问题,但通常解决方案是找到一种方法来确保失败不会失败。例如,数据库可能会在提交到外部数据结构或文件之前写入更改。如果事务失败,则可以丢弃文件/数据结构。它必须确保从外部结构/文件提交的更改是一个不会失败的原子事务。

务实的解决办法也许就是确保 一次又一次的失败在天文学上是不可能的,因为做东西 在某些情况下,失败几乎是不可能的。

对我来说,最合适的解决方案是以一种清理逻辑不会失败的方式编写非清理逻辑。例如,如果您想要创建一个新的数据结构来清理现有的数据结构,那么您可能会寻求提前创建那个辅助结构,这样我们就不必在析构函数中创建它了。

This is all much easier said than done, admittedly, but it's the only really proper way I see to go about it. Sometimes I think there should be an ability to write separate destructor logic for normal execution paths away from exceptional ones, since sometimes destructors feel a little bit like they have double the responsibilities by trying to handle both (an example is scope guards which require explicit dismissal; they wouldn't require this if they could differentiate exceptional destruction paths from non-exceptional ones).

Still the ultimate problem is that we can't fail to fail, and it's a hard conceptual design problem to solve perfectly in all cases. It does get easier if you don't get too wrapped up in complex control structures with tons of teeny objects interacting with each other, and instead model your designs in a slightly bulkier fashion (example: particle system with a destructor to destroy the entire particle system, not a separate non-trivial destructor per particle). When you model your designs at this kind of coarser level, you have less non-trivial destructors to deal with, and can also often afford whatever memory/processing overhead is required to make sure your destructors cannot fail.

And that's one of the easiest solutions naturally is to use destructors less often. In the particle example above, perhaps upon destroying/removing a particle, some things should be done that could fail for whatever reason. In that case, instead of invoking such logic through the particle's dtor which could be executed in an exceptional path, you could instead have it all done by the particle system when it removes a particle. Removing a particle might always be done during a non-exceptional path. If the system is destroyed, maybe it can just purge all particles and not bother with that individual particle removal logic which can fail, while the logic that can fail is only executed during the particle system's normal execution when it's removing one or more particles.

如果避免使用非平凡析构函数处理大量小对象,通常会出现这样的解决方案。当你被大量的小对象纠缠在一起时,你可能会陷入混乱,似乎不可能是异常安全的,这些小对象都有非平凡的dtor。

如果任何指定nothrow/noexcept的函数(包括应该继承其基类的noexcept规范的虚函数)试图调用任何可能抛出的函数,那么nothrow/noexcept实际上会被翻译成编译器错误,这将会有很大帮助。这样我们就能在编译时捕获所有这些东西,如果我们实际上无意中编写了一个析构函数,它可能会抛出。

其他回答

Martin Ba(上图)在正确的轨道上——你为RELEASE和COMMIT逻辑构建了不同的架构。

发布:

你应该吃任何错误。您正在释放内存、关闭连接等。系统中的任何人都不应该再看到这些东西,并且您正在将资源交还给操作系统。如果你看起来需要真正的错误处理,这可能是你的对象模型设计缺陷的结果。

提交:

This is where you want the same kind of RAII wrapper objects that things like std::lock_guard are providing for mutexes. With those you don't put the commit logic in the dtor AT ALL. You have a dedicated API for it, then wrapper objects that will RAII commit it in THEIR dtors and handle the errors there. Remember, you can CATCH exceptions in a destructor just fine; its issuing them that's deadly. This also lets you implement policy and different error handling just by building a different wrapper (e.g. std::unique_lock vs. std::lock_guard), and ensures you won't forget to call the commit logic- which is the only half-way decent justification for putting it in a dtor in the 1st place.

c++的ISO草案(ISO/IEC JTC 1/SC 22 N 4411)

因此,析构函数通常应该捕获异常,而不是让它们从析构函数传播出去。

为在try块到throw-的路径上构造的自动对象调用析构函数的过程 表达式称为“堆栈unwind”。[注意:如果在堆栈展开期间调用析构函数退出 异常,std::terminate被调用(15.5.1)。因此,析构函数通常应该捕获异常,而不是let 它们从析构函数中传播出去。-结束注]

关于从析构函数抛出,真正要问自己的问题是“调用者可以用它做什么?”你是否真的可以对异常做一些有用的事情,来抵消从析构函数抛出的危险?

如果我销毁了一个Foo对象,而Foo析构函数抛出了一个异常,我可以合理地对它做什么?我可以记录,也可以忽略。这是所有。我不能“修复”它,因为Foo对象已经消失了。最好的情况是,我记录异常并继续,就像什么都没有发生一样(或者终止程序)。这真的值得通过从析构函数抛出来潜在地引起未定义的行为吗?

其他人都解释了为什么抛出析构函数很糟糕……你能做些什么呢?如果您正在执行一个可能失败的操作,请创建一个单独的公共方法来执行清理,并可以抛出任意异常。在大多数情况下,用户会忽略这一点。如果用户希望监视清理的成功/失败,他们可以简单地调用显式清理例程。

例如:

class TempFile {
public:
    TempFile(); // throws if the file couldn't be created
    ~TempFile() throw(); // does nothing if close() was already called; never throws
    void close(); // throws if the file couldn't be deleted (e.g. file is open by another process)
    // the rest of the class omitted...
};

我目前遵循的策略(很多人都这么说)是,类不应该主动从析构函数抛出异常,而是应该提供一个公共的“关闭”方法来执行可能失败的操作……

...but I do believe destructors for container-type classes, like a vector, should not mask exceptions thrown from classes they contain. In this case, I actually use a "free/close" method that calls itself recursively. Yes, I said recursively. There's a method to this madness. Exception propagation relies on there being a stack: If a single exception occurs, then both the remaining destructors will still run and the pending exception will propagate once the routine returns, which is great. If multiple exceptions occur, then (depending on the compiler) either that first exception will propagate or the program will terminate, which is okay. If so many exceptions occur that the recursion overflows the stack then something is seriously wrong, and someone's going to find out about it, which is also okay. Personally, I err on the side of errors blowing up rather than being hidden, secret, and insidious.

关键是容器保持中立,由所包含的类决定它们是否从析构函数抛出异常。