C++ 从析构函数中抛出异常
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/130117/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
throwing exceptions out of a destructor
提问by Greg Rogers
Most people say neverthrow 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).
大多数人说永远不要从析构函数中抛出异常 - 这样做会导致未定义的行为。Stroustrup 指出“向量析构函数为每个元素显式调用析构函数。这意味着如果元素析构函数抛出,向量析构失败......实际上没有好的方法来防止从析构函数抛出异常,所以库不保证元素析构函数是否抛出“(来自附录 E3.2)。
This articleseems 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.
显然,这些类型的错误很少见,但也有可能。
采纳答案by Martin York
Throwing an exception out of a destructor is dangerous.
If another exception is already propagating the application will terminate.
从析构函数中抛出异常是危险的。
如果另一个异常已经在传播,应用程序将终止。
#include <iostream>
class Bad
{
public:
// Added the noexcept(false) so the code keeps its original meaning.
// Post C++11 destructors are by default `noexcept(true)` and
// this will (by default) call terminate if an exception is
// escapes the destructor.
//
// But this example is designed to show that terminate is called
// if two exceptions are propagating at the same time.
~Bad() noexcept(false)
{
throw 1;
}
};
class Bad2
{
public:
~Bad2()
{
throw 1;
}
};
int main(int argc, char* argv[])
{
try
{
Bad bad;
}
catch(...)
{
std::cout << "Print This\n";
}
try
{
if (argc > 3)
{
Bad bad; // This destructor will throw an exception that escapes (see above)
throw 2; // But having two exceptions propagating at the
// same time causes terminate to be called.
}
else
{
Bad2 bad; // The exception in this destructor will
// cause terminate to be called.
}
}
catch(...)
{
std::cout << "Never print this\n";
}
}
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.
close() 方法可能会引发异常。如果文件已打开,析构函数会调用 close(),但要确保任何异常都不会传播到析构函数之外。
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.
因此,如果文件对象的用户想要对与关闭文件相关的问题进行特殊处理,他们将手动调用 close() 并处理任何异常。另一方面,如果他们不关心,那么析构函数将被留下来处理这种情况。
Scott Myers has an excellent article about the subject in his book "Effective C++"
Scott Myers 在他的书“Effective C++”中有一篇关于这个主题的优秀文章
Edit:
编辑:
Apparently also in "More Effective C++"
Item 11: Prevent exceptions from leaving destructors
显然也在“更有效的 C++”条款
11:防止异常离开析构函数
回答by Gal Goldman
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.
抛出析构函数可能会导致崩溃,因为该析构函数可能会作为“堆栈展开”的一部分被调用。堆栈展开是在抛出异常时发生的过程。在这个过程中,从“try”到抛出异常之前所有被推入堆栈的对象都将被终止 -> 将调用它们的析构函数。并且在此过程中,不允许再次抛出异常,因为不可能同时处理两个异常,因此,这将引发对 abort() 的调用,程序将崩溃并且控制权将返回给操作系统。
回答by Martin Ba
We have to differentiatehere instead of blindly following generaladvice for specificcases.
我们必须在这里区分,而不是盲目地遵循针对特定情况的一般建议。
Note that the following ignoresthe 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:
当我们将类分成两种类型时,整个问题就变得更容易思考了。类 dtor 可以有两种不同的职责:
- (R) release semantics (aka free that memory)
- (C) commitsemantics (aka flushfile to disk)
- (R) 释放语义(又名释放内存)
- (C)提交语义(又名将文件刷新到磁盘)
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);
.
如果我们以这种方式看待这个问题,那么我认为可以认为 (R) 语义永远不应该导致 dtor 的异常,因为 a) 我们对此无能为力 b) 许多自由资源操作没有甚至提供错误检查,例如。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 cando something about the error (on the application level) and we really should not continue as if nothing happened.
具有 (C) 语义的对象,例如需要成功刷新其数据的文件对象或在 dtor 中执行提交的(“范围保护”)数据库连接是不同类型的:我们可以对错误(在应用程序级别),我们真的不应该像什么也没发生一样继续。
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.
如果我们遵循 RAII 路线并允许在其 d'tors 中具有 (C) 语义的对象,我认为我们还必须允许此类 d'tors 可以抛出的奇怪情况。因此,您不应该将此类对象放入容器中,并且terminate()
如果提交 dtor 抛出而另一个异常处于活动状态,则程序仍然可以。
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)
关于错误处理(提交/回滚语义)和异常,Andrei Alexandrescu发表了一篇精彩的演讲:C++ 中的错误处理/声明式控制流(在NDC 2014举行)
In the details, he explains how the Folly library implements an UncaughtExceptionCounter
for their ScopeGuard
tooling.
在细节中,他解释了 Folly 库如何UncaughtExceptionCounter
为他们的ScopeGuard
工具实现 an 。
(I should note that othersalso had similar ideas.)
(我应该注意到其他人也有类似的想法。)
While the talk doesn't focus on throwing from a d'tor, it shows a tool that can be used todayto get rid of the problems with when to throwfrom a d'tor.
虽然演讲的重点不是从 d'tor 投掷,但它展示了一种今天可以使用的工具来摆脱何时从 d'tor投掷的问题。
In the future, there maybe a std feature for this, see N3614,and a discussion about it.
在未来,有可能是性病的特征这一点,看到N3614,以及关于它的讨论。
Upd '17: The C++17 std feature for this is std::uncaught_exceptions
afaikt. I'll quickly quote the cppref article:
更新 17 年:C++17 标准特性是std::uncaught_exceptions
afaikt。我将快速引用 cppref 文章:
Notes
An example where
int
-returninguncaught_exceptions
is used is ... ... first creates a guard object and records the number of uncaught exceptions in its constructor. The output is performed by the guard object's destructor unless foo() throws (in which case the number of uncaught exceptions in the destructor is greater than what the constructor observed)
笔记
使用
int
-returning 的一个例子uncaught_exceptions
是 ... ... 首先创建一个保护对象并在其构造函数中记录未捕获异常的数量。输出由保护对象的析构函数执行,除非 foo() 抛出(在这种情况下,析构函数中未捕获的异常数量大于构造函数观察到的数量)
回答by Derek Park
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 the Foo
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 the Foo
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?
如果我销毁一个Foo
对象,并且Foo
析构函数抛出一个异常,我可以合理地用它做什么?我可以记录它,或者我可以忽略它。就这样。我无法“修复”它,因为Foo
对象已经消失了。最好的情况是,我记录异常并继续,好像什么也没发生(或终止程序)。这真的值得通过从析构函数中抛出来潜在地导致未定义的行为吗?
回答by Doug T.
Its dangerous, but it also doesn't make sense from a readability/code understandability standpoint.
它很危险,但从可读性/代码可理解性的角度来看,它也没有意义。
What you have to ask is in this situation
你要问的是在这种情况下
int foo()
{
Object o;
// As foo exits, o's destructor is called
}
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.
什么应该捕获异常?应该是 foo 的调用者吗?或者应该 foo 处理它?为什么 foo 的调用者应该关心 foo 内部的一些对象?语言可能有一种方式来定义它以使其有意义,但它会变得不可读且难以理解。
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.
更重要的是,Object 的内存去哪儿了?对象拥有的内存去哪儿了?它是否仍然分配(表面上是因为析构函数失败)?还要考虑对象在堆栈空间中,因此无论如何它显然都消失了。
Then consider this case
那么考虑这个案例
class Object
{
Object2 obj2;
Object3* obj3;
virtual ~Object()
{
// What should happen when this fails? How would I actually destroy this?
delete obj3;
// obj 2 fails to destruct when it goes out of scope, now what!?!?
// should the exception propogate?
}
};
When the delete of obj3 fails, how do I actually delete in a way that is guaranteed to not fail? Its my memory dammit!
当obj3的删除失败时,我如何以保证不会失败的方式实际删除?这是我的记忆该死的!
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.
现在考虑在第一个代码片段中 Object 会自动消失,因为它在堆栈上而 Object3 在堆上。由于指向 Object3 的指针消失了,您有点像 SOL。你有内存泄漏。
Now one safe way to do things is the following
现在一种安全的做事方式如下
class Socket
{
virtual ~Socket()
{
try
{
Close();
}
catch (...)
{
// Why did close fail? make sure it *really* does close here
}
}
};
Also see this FAQ
另请参阅此常见问题解答
回答by lothar
From the ISO draft for C++ (ISO/IEC JTC 1/SC 22 N 4411)
来自 C++ 的 ISO 草案 (ISO/IEC JTC 1/SC 22 N 4411)
So destructors should generally catch exceptions and not let them propagate out of the destructor.
所以析构函数通常应该捕获异常,而不是让它们传播到析构函数之外。
3 The process of calling destructors for automatic objects constructed on the path from a try block to a throw- expression is called “stack unwinding.” [ Note: If a destructor called during stack unwinding exits with an exception, std::terminate is called (15.5.1). So destructors should generally catch exceptions and not let them propagate out of the destructor. — end note ]
3 为在从 try 块到 throw 表达式的路径上构造的自动对象调用析构函数的过程称为“堆栈展开”。[ 注意:如果在堆栈展开期间调用的析构函数以异常退出,则调用 std::terminate (15.5.1)。所以析构函数通常应该捕获异常,而不是让它们传播到析构函数之外。— 尾注 ]
回答by Franci Penov
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.
您的析构函数可能正在其他析构函数链中执行。抛出一个没有被直接调用者捕获的异常会使多个对象处于不一致的状态,从而导致更多的问题,然后忽略清理操作中的错误。
回答by GaspardP
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 with noexcept
.
我所在的小组认为在析构函数中抛出的“范围保护”模式在许多情况下都很有用 - 特别是对于单元测试。但是,请注意,在 C++11 中,抛出析构函数会导致调用 ,std::terminate
因为析构函数使用noexcept
.
Andrzej Krzemieński has a great post on the topic of destructors that throw:
Andrzej Krzemieński 有一篇关于抛出的析构函数主题的精彩帖子:
He points out that C++11 has a mechanism to override the default noexcept
for destructors:
他指出 C++11 有一种机制来覆盖noexcept
析构函数的默认值:
In C++11, a destructor is implicitly specified as
noexcept
. Even if you add no specification and define your destructor like this:class MyType { public: ~MyType() { throw Exception(); } // ... };
The compiler will still invisibly add specification
noexcept
to your destructor. And this means that the moment your destructor throws an exception,std::terminate
will be called, even if there was no double-exception situation. If you are really determined to allow your destructors to throw, you will have to specify this explicitly; you have three options:
- Explicitly specify your destructor as
noexcept(false)
,- Inherit your class from another one that already specifies its destructor as
noexcept(false)
.- Put a non-static data member in your class that already specifies its destructor as
noexcept(false)
.
在 C++11 中,析构函数被隐式指定为
noexcept
. 即使您没有添加任何规范并像这样定义析构函数:class MyType { public: ~MyType() { throw Exception(); } // ... };
编译器仍然会无形地
noexcept
向析构函数添加规范。这意味着在您的析构函数抛出异常的那一刻,std::terminate
将被调用,即使没有双异常情况。如果您真的决定允许析构函数抛出异常,则必须明确指定;你有三个选择:
- 明确指定您的析构函数为
noexcept(false)
,- 从另一个已经将其析构函数指定为的类继承您的类
noexcept(false)
。- 在您的类中放置一个非静态数据成员,该成员已将其析构函数指定为
noexcept(false)
.
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 using std::uncaught_exception()
.
最后,如果您决定在析构函数中抛出异常,则应始终注意双重异常的风险(在堆栈因异常而展开时抛出)。这会导致调用std::terminate
并且它很少是您想要的。为了避免这种行为,您可以简单地检查是否已经存在异常,然后再使用std::uncaught_exception()
.
回答by Tom
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:
例如:
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...
};
回答by DJClayworth
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?
问题在于,它列出的替代方案中的所有问题都没有异常行为那么糟糕,让我们记住,这是“程序的未定义行为”。作者的一些反对意见包括“审美丑陋”和“鼓励不良风格”。现在你更愿意拥有哪一个?一个风格不好的程序,或者一个表现出未定义行为的程序?