如果您不应该在析构函数中抛出异常,那么如何处理其中的错误?
大多数人说永远从析构函数中抛出异常 - 这样做会导致未定义的行为。 Stroustrup 指出“向量析构函数显式地调用每个元素的析构函数。这意味着如果元素析构函数抛出异常,向量析构就会失败......确实没有好的方法来防止析构函数抛出异常,因此,库不保证元素析构函数是否抛出“(来自附录 E3.2)。
这篇文章似乎另有说法 - 抛出析构函数或多或少少一点还好。
所以我的问题是 - 如果从析构函数中抛出导致未定义的行为,您如何处理析构函数期间发生的错误?
如果在清理操作期间发生错误,您会忽略它吗? 如果这是一个可以在堆栈中处理但不能在析构函数中处理的错误,那么从析构函数中抛出异常是否有意义?
显然,此类错误很少见,但也有可能发生。
Most people say never throw an exception out of a destructor - doing so results in undefined behavior. Stroustrup makes the point that "the vector destructor explicitly invokes the destructor for every element. This implies that if an element destructor throws, the vector destruction fails... There is really no good way to protect against exceptions thrown from destructors, so the library makes no guarantees if an element destructor throws" (from Appendix E3.2).
This article seems to say otherwise - that throwing destructors are more or less okay.
So my question is this - if throwing from a destructor results in undefined behavior, how do you handle errors that occur during a destructor?
If an error occurs during a cleanup operation, do you just ignore it? If it is an error that can potentially be handled up the stack but not right in the destructor, doesn't it make sense to throw an exception out of the destructor?
Obviously these kinds of errors are rare, but possible.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(18)
主要问题是:你不能失败。 失败到底意味着什么? 如果向数据库提交事务失败,并且失败(无法回滚),我们数据的完整性会发生什么情况?
由于析构函数是为正常和异常(失败)路径调用的,因此它们本身不能失败,否则我们就会“失败”。
这是一个概念上的困难问题,但解决方案通常就是找到一种方法来确保失败不会失败。 例如,数据库可能会在提交到外部数据结构或文件之前写入更改。 如果事务失败,则文件/数据结构可以被丢弃。 然后它必须确保从外部结构/文件提交更改是一个不会失败的原子事务。
对我来说最合适的解决方案是以一种使清理逻辑不会失败的方式编写非清理逻辑。 例如,如果您想创建一个新的数据结构以清理现有的数据结构,那么您可能会寻求提前创建该辅助结构,以便我们不再需要在析构函数内创建它。
诚然,这说起来容易做起来难,但这是我认为唯一真正正确的方法。 有时我认为应该有能力为正常执行路径编写单独的析构函数逻辑,远离异常路径,因为有时析构函数感觉有点像通过尝试处理这两者而承担了双重责任(一个例子是需要显式解雇的作用域守卫) ;如果他们能够区分特殊的破坏路径和非特殊的破坏路径,他们就不需要这样做)。
最终的问题仍然是我们不能失败,而且这是一个在所有情况下都需要完美解决的概念设计难题。 如果您不要过于陷入复杂的控制结构(其中有大量相互交互的微小对象),而是以稍微庞大的方式对您的设计进行建模(例如:带有析构函数的粒子系统来破坏整个粒子),那么它确实会变得更容易系统,而不是每个粒子一个单独的非平凡析构函数)。 当您在这种较粗糙的级别上对设计进行建模时,需要处理的重要析构函数就会减少,并且通常还可以承担确保析构函数不会失败所需的任何内存/处理开销。
最简单的解决方案之一自然是减少使用析构函数。 在上面的粒子示例中,也许在销毁/移除粒子时,应该执行一些可能因任何原因而失败的操作。 在这种情况下,您可以在删除粒子时让粒子系统完成所有操作,而不是通过粒子的 dtor 调用此类逻辑(可以在异常路径中执行)。 移除粒子可能总是在非异常路径中完成。 如果系统被破坏,也许它可以只清除所有粒子,而不必担心可能失败的单个粒子移除逻辑,而可能失败的逻辑仅在粒子系统正常执行期间移除一个或多个粒子时执行。
如果您避免使用不平凡的析构函数处理大量微小对象,通常会出现类似的解决方案。 你可能会陷入混乱,似乎几乎不可能出现异常安全,那就是当你确实陷入许多微小的对象,而这些对象都有不平凡的 dtors 时。
如果任何指定它的东西(包括应该继承其基类的 noexcept 规范的虚函数)尝试调用任何可能抛出的东西,那么如果 nothrow/noexcept 实际上转换为编译器错误,那将会有很大帮助。 这样,如果我们实际上无意中编写了一个可能抛出异常的析构函数,我们就能够在编译时捕获所有这些东西。
The main problem is this: you can't fail to fail. What does it mean to fail to fail, after all? If committing a transaction to a database fails, and it fails to fail (fails to rollback), what happens to the integrity of our data?
Since destructors are invoked for both normal and exceptional (fail) paths, they themselves cannot fail or else we're "failing to fail".
This is a conceptually difficult problem but often the solution is to just find a way to make sure that failing cannot fail. For example, a database might write changes prior to committing to an external data structure or file. If the transaction fails, then the file/data structure can be tossed away. All it has to then ensure is that committing the changes from that external structure/file an atomic transaction that can't fail.
The most proper solution to me is to write your non-cleanup logic in a way such that the cleanup logic can't fail. For example, if you're tempted to create a new data structure in order to clean up an existing data structure, then perhaps you might seek to create that auxiliary structure in advance so that we no longer have to create it inside a destructor.
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.
There are often solutions like that which crop up if you avoid dealing with lots of teeny objects with non-trivial destructors. Where you can get tangled up in a mess where it seems almost impossible to be exception-safety is when you do get tangled up in lots of teeny objects that all have non-trivial dtors.
It would help a lot if nothrow/noexcept actually translated into a compiler error if anything which specifies it (including virtual functions which should inherit the noexcept specification of its base class) attempted to invoke anything that could throw. This way we'd be able to catch all this stuff at compile-time if we actually write a destructor inadvertently which could throw.
我们必须在这里区分,而不是盲目遵循针对具体情况的一般建议。
请注意,以下忽略对象容器的问题以及面对容器内对象的多个d'tors时该怎么办。 (它可以被部分忽略,因为有些对象不适合放入容器中。)
当我们将类分成两种类型时,整个问题就变得更容易思考。 类 dtor 可以有两种不同的职责:
如果我们查看以这种方式提问,那么我认为可以说(R)语义永远不应该导致 dtor 的异常,因为 a)我们对此无能为力,b)许多免费资源操作甚至不提供错误检查,例如
void
free(void* p);
。具有 (C) 语义的对象,例如需要成功刷新其数据的文件对象或在 dtor 中进行提交的(“范围保护”)数据库连接,属于不同类型:我们可以对错误采取一些措施(在应用程序级别),我们真的不应该继续下去,就好像什么都没发生一样。
如果我们遵循 RAII 路线并允许在其 d'tors 中具有 (C) 语义的对象,我认为我们还必须允许此类 d'tors 可能抛出的奇怪情况。 因此,您不应该将此类对象放入容器中,并且如果 commit-dtor 在另一个异常处于活动状态时抛出,程序仍然可以
terminate()
。关于错误处理(提交/回滚语义)和异常,Andrei Alexandrescu 发表了一篇很好的演讲:C++/声明式控制流中的错误处理(位于 NDC 2014)
在详细信息中,他解释了 Folly 库如何实现
UncaughtExceptionCounter
为他们的ScopeGuard
工具。(我应该注意到其他人也有类似的想法。)
虽然谈话的重点不是从作为一名作者,它展示了一个现在可以用来消除何时从 d'tor 抛出的问题。
在
未来中,可能为此提供标准功能,请参阅N3614、和 有关它的讨论。更新 '17:C++17 std 功能为
std::uncaught_exceptions
事实证明。 我将快速引用 cppref 文章:We have to differentiate here instead of blindly following general advice for specific cases.
Note that the following ignores the issue of containers of objects and what to do in the face of multiple d'tors of objects inside containers. (And it can be ignored partially, as some objects are just no good fit to put into a container.)
The whole problem becomes easier to think about when we split classes in two types. A class dtor can have two different responsibilities:
If we view the question this way, then I think that it can be argued that (R) semantics should never cause an exception from a dtor as there is a) nothing we can do about it and b) many free-resource operations do not even provide for error checking, e.g.
void
free(void* p);
.Objects with (C) semantics, like a file object that needs to successfully flush it's data or a ("scope guarded") database connection that does a commit in the dtor are of a different kind: We can do something about the error (on the application level) and we really should not continue as if nothing happened.
If we follow the RAII route and allow for objects that have (C) semantics in their d'tors I think we then also have to allow for the odd case where such d'tors can throw. It follows that you should not put such objects into containers and it also follows that the program can still
terminate()
if a commit-dtor throws while another exception is active.With regard to error handling (Commit / Rollback semantics) and exceptions, there is a good talk by one Andrei Alexandrescu: Error Handling in C++ / Declarative Control Flow (held at NDC 2014)
In the details, he explains how the Folly library implements an
UncaughtExceptionCounter
for theirScopeGuard
tooling.(I should note that others also had similar ideas.)
While the talk doesn't focus on throwing from a d'tor, it shows a tool that can be used today to get rid of the problems with when to throw from a d'tor.
In the
future, theremaybe a std feature for this,see N3614,and a discussion about it.Upd '17: The C++17 std feature for this is
std::uncaught_exceptions
afaikt. I'll quickly quote the cppref article:它很危险,但从可读性/代码可理解性的角度来看,它也没有意义。
你要问的是在这种情况下
应该用什么来捕获异常呢? foo 的调用者应该吗? 或者 foo 应该处理它吗? 为什么 foo 的调用者应该关心 foo 内部的某些对象? 语言可能有某种方式来定义它,使其有意义,但它会变得不可读且难以理解。
更重要的是,Object的内存去了哪里? 对象拥有的内存去了哪里? 它是否仍然被分配(表面上是因为析构函数失败)? 还要考虑该对象位于堆栈空间中,因此无论如何它显然都消失了。
那么考虑这种情况,
当obj3的删除失败时,我该如何以保证不失败的方式实际删除呢? 这是我的记忆该死!
现在考虑在第一个代码片段中,Object 会自动消失,因为它位于堆栈上,而 Object3 位于堆上。 由于指向 Object3 的指针消失了,您就可以了。 你有内存泄漏。
现在,一种安全的做法如下
另请参阅此常见问题解答< /a>
Its dangerous, but it also doesn't make sense from a readability/code understandability standpoint.
What you have to ask is in this situation
What should catch the exception? Should the caller of foo? Or should foo handle it? Why should the caller of foo care about some object internal to foo? There might be a way the language defines this to make sense, but its going to be unreadable and difficult to understand.
More importantly, where does the memory for Object go? Where does the memory the object owned go? Is it still allocated (ostensibly because the destructor failed)? Consider also the object was in stack space, so its obviously gone regardless.
Then consider this case
When the delete of obj3 fails, how do I actually delete in a way that is guaranteed to not fail? Its my memory dammit!
Now consider in the first code snippet Object goes away automatically because its on the stack while Object3 is on the heap. Since the pointer to Object3 is gone, you're kind of SOL. You have a memory leak.
Now one safe way to do things is the following
Also see this FAQ
关于从析构函数中抛出,真正要问自己的问题是“调用者可以用它做什么?” 实际上,您可以对异常做一些有用的事情,以抵消从析构函数抛出异常所造成的危险吗?
如果我销毁一个
Foo
对象,并且Foo
析构函数抛出异常,我可以合理地用它做什么? 我可以记录它,也可以忽略它。 就这样。 我无法“修复”它,因为Foo
对象已经消失了。 最好的情况是,我记录异常并继续,就像什么都没发生一样(或终止程序)。 这真的值得通过从析构函数中抛出来潜在地导致未定义的行为吗?The real question to ask yourself about throwing from a destructor is "What can the caller do with this?" Is there actually anything useful you can do with the exception, that would offset the dangers created by throwing from a destructor?
If I destroy a
Foo
object, and theFoo
destructor tosses out an exception, what I can reasonably do with it? I can log it, or I can ignore it. That's all. I can't "fix" it, because theFoo
object is already gone. Best case, I log the exception and continue as if nothing happened (or terminate the program). Is that really worth potentially causing undefined behavior by throwing from a destructor?来自 C++ 的 ISO 草案 (ISO/IEC JTC 1/SC 22 N 4411)
因此,析构函数通常应该捕获异常,而不是让它们从析构函数中传播出去。
From the ISO draft for C++ (ISO/IEC JTC 1/SC 22 N 4411)
So destructors should generally catch exceptions and not let them propagate out of the destructor.
我属于认为在析构函数中抛出“范围保护”模式在许多情况下都很有用的小组 - 特别是对于单元测试。 但是,请注意,在 C++11 中,抛出析构函数会导致调用
std::terminate
,因为析构函数是用noexcept
隐式注释的。Andrzej Krzemieński 有一篇关于抛出析构函数主题的精彩文章:
他指出C++11有一种机制可以覆盖析构函数的默认
noexcept
:最后,如果您确实决定抛出析构函数,则应该始终注意双重异常的风险(由于异常而在堆栈展开时抛出)。 这会导致调用
std::terminate
,而这很少是您想要的。 为了避免这种行为,您可以简单地使用std::uncaught_exception()
在抛出新异常之前检查是否已经存在异常。因此,一个完整的工作示例可能如下所示:
I am in the group that considers that the "scoped guard" pattern throwing in the destructor is useful in many situations - particularly for unit tests. However, be aware that in C++11, throwing in a destructor results in a call to
std::terminate
since destructors are implicitly annotated withnoexcept
.Andrzej Krzemieński has a great post on the topic of destructors that throw:
He points out that C++11 has a mechanism to override the default
noexcept
for destructors:Finally, if you do decide to throw in the destructor, you should always be aware of the risk of a double-exception (throwing while the stack is being unwind because of an exception). This would cause a call to
std::terminate
and it is rarely what you want. To avoid this behaviour, you can simply check if there is already an exception before throwing a new one usingstd::uncaught_exception()
.So a full working example might look like this:
作为对主要答案的补充,这些答案很好,全面且准确,我想对您引用的文章发表评论 - 那篇文章说“在析构函数中抛出异常并不是那么糟糕”。
本文采用“抛出异常的替代方案是什么”这一行,并列出了每种替代方案的一些问题。 这样做后,得出的结论是,因为我们找不到没有问题的替代方案,所以我们应该继续抛出异常。
问题在于,它列出的替代方案中的任何问题都不如异常行为那么糟糕,让我们记住,异常行为是“程序的未定义行为”。 作者的一些反对意见包括“审美丑陋”和“鼓励不良风格”。 现在你更想要哪一个? 一个风格不好的程序,或者表现出未定义行为的程序?
As an addition to the main answers, which are good, comprehensive and accurate, I would like to comment about the article you reference - the one that says "throwing exceptions in destructors is not so bad".
The article takes the line "what are the alternatives to throwing exceptions", and lists some problems with each of the alternatives. Having done so it concludes that because we can't find a problem-free alternative we should keep throwing exceptions.
The trouble is is that none of the problems it lists with the alternatives are anywhere near as bad as the exception behaviour, which, let's remember, is "undefined behaviour of your program". Some of the author's objections include "aesthetically ugly" and "encourage bad style". Now which would you rather have? A program with bad style, or one which exhibited undefined behaviour?
您的析构函数可能在其他析构函数链内执行。 引发直接调用者未捕获的异常可能会使多个对象处于不一致状态,从而导致比忽略清理操作中的错误更多的问题。
Your destructor might be executing inside a chain of other destructors. Throwing an exception that is not caught by your immediate caller can leave multiple objects in an inconsistent state, thus causing even more problems then ignoring the error in the cleanup operation.
其他人都解释了为什么抛出析构函数是可怕的......你能做些什么呢? 如果您正在执行的操作可能会失败,请创建一个单独的公共方法来执行清理并可以引发任意异常。 在大多数情况下,用户会忽略这一点。 如果用户想要监视清理的成功/失败,他们可以简单地调用显式清理例程。
例如:
Everyone else has explained why throwing destructors are terrible... what can you do about it? If you're doing an operation that may fail, create a separate public method that performs cleanup and can throw arbitrary exceptions. In most cases, users will ignore that. If users want to monitor the success/failure of the cleanup, they can simply call the explicit cleanup routine.
For example:
答:有几种选择:
让异常从析构函数中流出,不管其他地方发生了什么。 这样做时要注意(甚至担心)std::terminate 可能会随之而来。
永远不要让异常从析构函数中流出。 如果可以的话,可能会写入日志,一些大红色的错误文本。
我的最爱:如果
std::uncaught_exception
返回false,让你的异常流出。 如果它返回 true,则返回到日志记录方法。但是投入 d'tors 是好事吗?
我同意上面的大部分内容,即最好在析构函数中避免抛出,如果可以的话。 但有时你最好接受它可能发生的事实,并妥善处理它。 我选上面3个。
在一些奇怪的情况下,从析构函数中抛出实际上是一个好主意。
就像“必须检查”错误代码一样。 这是从函数返回的值类型。 如果调用者读取/检查包含的错误代码,则返回的值将默默地破坏。
但是,如果返回值超出范围时尚未读取返回的错误代码,会从其析构函数中抛出一些异常。
A: There are several options:
Let the exceptions flow out of your destructor, regardless of what's going on elsewhere. And in doing so be aware (or even fearful) that std::terminate may follow.
Never let exception flow out of your destructor. May be write to a log, some big red bad text if you can.
my fave : If
std::uncaught_exception
returns false, let you exceptions flow out. If it returns true, then fall back to the logging approach.But is it good to throw in d'tors?
I agree with most of the above that throwing is best avoided in destructor, where it can be. But sometimes you're best off accepting it can happen, and handle it well. I'd choose 3 above.
There are a few odd cases where its actually a great idea to throw from a destructor.
Like the "must check" error code. This is a value type which is returned from a function. If the caller reads/checks the contained error code, the returned value destructs silently.
But, if the returned error code has not been read by the time the return values goes out of scope, it will throw some exception, from its destructor.
设置警报事件。 通常,警报事件是在清理对象时通知失败的更好形式
Set an alarm event. Typically alarm events are better form of notifying failure while cleaning up objects
从析构函数中抛出异常永远不会导致未定义的行为。
将异常抛出析构函数的问题是,在处理未捕获的异常时(在创建异常对象之后,直到异常激活的处理程序完成),成功创建的对象的析构函数由异常处理调用,这些对象的作用域正在离开机制; 并且,如果在处理未捕获的异常时调用的析构函数中的此类附加异常中断了对未捕获的异常的处理,则会导致调用
std::terminate
(另一种情况是std::exception
code> 被调用的原因是异常不被任何处理程序处理,但这与任何其他函数一样,无论它是否是析构函数)。如果正在处理未捕获的异常,您的代码永远不知道附加异常是否会被捕获,或者是否会存档未捕获的异常处理机制,因此永远不知道抛出是否安全。
不过,可以知道处理未捕获的异常正在进行中( https:// en.cppreference.com/w/cpp/error/uncaught_exception),因此您可以通过检查条件来矫枉过正,并仅在情况并非如此时才抛出(在某些情况下,它不会抛出安全的)。
但在实践中,这种分成两种可能的行为是没有用的——它只是无助于你制作一个设计良好的程序。
如果您抛出析构函数而忽略是否正在进行未捕获的异常处理,为了避免可能调用 std::terminate,您必须保证在对象的生命周期中抛出的所有异常都可能发生。在开始销毁对象之前,会捕获从析构函数抛出的异常。
它的用途相当有限; 您几乎无法使用所有可以合理地允许以这种方式从其析构函数中抛出的类; 仅对某些类允许此类例外以及对这些类的使用进行如此严格的限制的组合也妨碍了设计良好的程序。
Throwing an exception out of a destructor never causes undefined behaviour.
The problem of throwing exceptions out a destructor is that destructors of successfully created objects which scopes are leaving while handling an uncaught exception (it is after an exception object is created and until completion of a handler of the exception activation), are called by exception handling mechanism; and, If such additional exception from the destructor called while processing the uncaught exception interrupts handling the uncaught exception, it will cause calling
std::terminate
(the other case whenstd::exception
is called is that an exception is not handled by any handler but this is as for any other function, regardless of whether or not it was a destructor).If handling an uncaught exception in progress, your code never knows whether the additional exception will be caught or will archive an uncaught exception handling mechanism, so never know definitely whether it is safe to throw or not.
Though, it is possible to know that handling an uncaught exception is in progress ( https://en.cppreference.com/w/cpp/error/uncaught_exception), so you are able to overkill by checking the condition and throw only if it is not the case (it will not throw in some cases when it would be safe).
But in practice such separating into two possible behaviours is not useful - it just does not help you to make a well-designed program.
If you throw out of destructors ignoring whether or not an uncaught exception handling is in progress, in order to avoid possible calling
std::terminate
, you must guarantee that all exceptions thrown during lifetime of an object that may throw an exception from their destructor are caught before beginning of destruction of the object.It is quite limited usage; you hardly can use all classes which would be reasonably allowed to throw out of their destructor in this way; and a combination of allowing such exceptions only for some classes with such restricted usage of these classes impede making a well-designed program, too.
与构造函数不同的是,在构造函数中抛出异常可以是指示对象创建成功的有用方法,而析构函数中不应抛出异常。
当在堆栈展开过程中从析构函数引发异常时,就会出现此问题。 如果发生这种情况,编译器就会陷入不知道是否继续堆栈展开过程或处理新异常的情况。 最终的结果是你的程序将立即终止。
因此,最好的做法就是完全避免在析构函数中使用异常。 相反,将消息写入日志文件。
Unlike constructors, where throwing exceptions can be a useful way to indicate that object creation succeeded, exceptions should not be thrown in destructors.
The problem occurs when an exception is thrown from a destructor during the stack unwinding process. If that happens, the compiler is put in a situation where it doesn’t know whether to continue the stack unwinding process or handle the new exception. The end result is that your program will be terminated immediately.
Consequently, the best course of action is just to abstain from using exceptions in destructors altogether. Write a message to a log file instead.
Martin Ba(上图)走在正确的轨道上——您以不同的方式构建 RELEASE 和 COMMIT 逻辑。
对于发布:
您应该吃掉任何错误。 您正在释放内存、关闭连接等。系统中的其他任何人都不应该再次看到这些东西,并且您正在将资源交还给操作系统。 如果您看起来需要在这里进行真正的错误处理,则可能是对象模型中设计缺陷的结果。
对于提交:
这是您需要与 std::lock_guard 等为互斥体提供的同类 RAII 包装对象的地方。 对于这些,您根本不需要将提交逻辑放入 dtor 中。 您有一个专用的 API,然后是包装对象,RAII 将其提交到他们的 dtor 中并在那里处理错误。 请记住,您可以在析构函数中捕获异常; 发布它们是致命的。 这还允许您通过构建不同的包装器(例如 std::unique_lock 与 std::lock_guard)来实现策略和不同的错误处理,并确保您不会忘记调用提交逻辑 - 这是唯一的中间方法首先将其放入 dtor 的合理理由。
Martin Ba (above) is on the right track- you architect differently for RELEASE and COMMIT logic.
For Release:
You should eat any errors. You're freeing memory, closing connections, etc. Nobody else in the system should ever SEE those things again, and you're handing back resources to the OS. If it looks like you need real error handling here, its likely a consequence of design flaws in your object model.
For 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.
显然,在非常特殊的情况下,例如下面的场景,您可以在析构函数中抛出异常并捕获它以正确处理它。
代码:
输出:
Apparently, in very special cases such as the following scenario, you CAN throw an exception in destructor and catch it to handle it properly.
Code:
Output:
我目前遵循的政策(很多人都在说),类不应主动从其析构函数中抛出异常,而应提供一个公共“关闭”方法来执行可能失败的操作......
但我确实相信容器类型类的析构函数(如向量)不应掩盖从它们包含的类抛出的异常。 在这种情况下,我实际上使用了递归调用自身的“free/close”方法。 是的,我递归地说。 这种疯狂是有办法的。 异常传播依赖于堆栈:如果发生单个异常,则剩余的析构函数仍将运行,并且一旦例程返回,待处理的异常将传播,这很棒。 如果发生多个异常,则(取决于编译器)要么传播第一个异常,要么程序终止,这是可以的。 如果发生太多异常,导致递归溢出堆栈,那么就会出现严重错误,有人会发现它,这也没关系。 就我个人而言,我宁愿错误被爆发,也不愿被隐藏、秘密和阴险。
关键是容器保持中立,并且由所包含的类来决定它们在从析构函数中抛出异常方面是否行为不当。
I currently follow the policy (that so many are saying) that classes shouldn't actively throw exceptions from their destructors but should instead provide a public "close" method to perform the operation that could fail...
...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.
The point is that the container remains neutral, and it's up to the contained classes to decide whether they behave or misbehave with regard to throwing exceptions from their destructors.
从析构函数中抛出异常是危险的。
如果另一个异常已经传播,应用程序将终止。
但是说“终止”是大多数编译器的一个非常明确的行为,因此它几乎从来不是“未定义的行为”(取决于编译器)。
这基本上可以归结为:
任何危险的事情(即可能引发异常)都应该通过公共方法(不一定是直接的)完成。 然后,类的用户可以通过使用公共方法并捕获任何潜在的异常来处理这些情况。
然后,析构函数将通过调用这些方法来结束对象(如果用户没有明确执行此操作),但抛出的任何异常都会被捕获并丢弃(在尝试解决问题之后)。
因此,实际上您将责任转移给了用户。 如果用户能够纠正异常,他们将手动调用适当的函数并处理任何错误。 如果对象的用户不担心(因为对象将被销毁),那么析构函数就可以处理事务。
示例:
std::fstream
close() 方法可能会引发异常。
如果文件已打开,析构函数将调用 close(),但确保任何异常不会传播到析构函数之外。
因此,如果文件对象的用户想要对与关闭文件相关的问题进行特殊处理,他们将手动调用 close() 并处理任何异常。 另一方面,如果他们不在乎,那么析构函数将被留下来处理这种情况。
Scott Meyers 在他的书“Effective C++”中有一篇关于该主题的精彩文章
编辑:
显然也在“更有效的 C++”中
第 11 项:防止异常离开析构函数
Throwing an exception out of a destructor is dangerous.
If another exception is already propagating the application will terminate.
But said "terminate" is a very well specified behaviour of majority of compilers, hence it's almost never "Undefined Behaviour" (depending on compiler).
This basically boils down to:
Anything dangerous (i.e. that could throw an exception) should be done via public methods (not necessarily directly). The user of your class can then potentially handle these situations by using the public methods and catching any potential exceptions.
The destructor will then finish off the object by calling these methods (if the user did not do so explicitly), but any exceptions throw are caught and dropped (after attempting to fix the problem).
So in effect you pass the responsibility onto the user. If the user is in a position to correct exceptions they will manually call the appropriate functions and processes any errors. If the user of the object is not worried (as the object will be destroyed) then the destructor is left to take care of business.
An example:
std::fstream
The close() method can potentially throw an exception.
The destructor calls close() if the file has been opened but makes sure that any exceptions do not propagate out of the destructor.
So if the user of a file object wants to do special handling for problems associated to closing the file they will manually call close() and handle any exceptions. If on the other hand they do not care then the destructor will be left to handle the situation.
Scott Meyers has an excellent article about the subject in his book "Effective C++"
Edit:
Apparently also in "More Effective C++"
Item 11: Prevent exceptions from leaving destructors
抛出析构函数可能会导致崩溃,因为该析构函数可能被称为“堆栈展开”的一部分。
堆栈展开是抛出异常时发生的过程。
在这个过程中,从“try”开始直到抛出异常为止所有被压入堆栈的对象都将被终止 -> 他们的析构函数将被调用。
在此过程中,不允许再次抛出异常,因为不可能一次处理两个异常,因此,这将引发对 abort() 的调用,程序将崩溃,控制权将返回到操作系统。
Throwing out of a destructor can result in a crash, because this destructor might be called as part of "Stack unwinding".
Stack unwinding is a procedure which takes place when an exception is thrown.
In this procedure, all the objects that were pushed into the stack since the "try" and until the exception was thrown, will be terminated -> their destructors will be called.
And during this procedure, another exception throw is not allowed, because it's not possible to handle two exceptions at a time, thus, this will provoke a call to abort(), the program will crash and the control will return to the OS.