throwing exceptions out of a destructor

2018-12-31 04:34发布

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.

16条回答
何处买醉
2楼-- · 2018-12-31 04:45

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.

查看更多
流年柔荑漫光年
3楼-- · 2018-12-31 04:45

So my question is this - if throwing from a destructor results in undefined behavior, how do you handle errors that occur during a destructor?

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 pragmatic solution is perhaps just make sure that the chances of failing on failure are astronomically improbable, since making things impossible to fail to fail can be almost impossible in some cases.

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.

查看更多
一个人的天荒地老
4楼-- · 2018-12-31 04:46

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:

  • (R) release semantics (aka free that memory)
  • (C) commit semantics (aka flush file to disk)

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 their ScopeGuard 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, there may be 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:

Notes

An example where int-returning uncaught_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)

查看更多
后来的你喜欢了谁
5楼-- · 2018-12-31 04:47

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.

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:

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).

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().

查看更多
零度萤火
6楼-- · 2018-12-31 04:49

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.

查看更多
大哥的爱人
7楼-- · 2018-12-31 04:51

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...
};
查看更多
登录 后发表回答