throwing exceptions out of a destructor

后端 未结 16 1841
暗喜
暗喜 2020-11-22 00:23

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 e

相关标签:
16条回答
  • 2020-11-22 00:56

    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.

    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

    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!

    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

    class Socket
    {
        virtual ~Socket()
        {
          try 
          {
               Close();
          }
          catch (...) 
          {
              // Why did close fail? make sure it *really* does close here
          }
        } 
    
    };
    

    Also see this FAQ

    0 讨论(0)
  • 2020-11-22 01:02

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

    A: There are several options:

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

    2. Never let exception flow out of your destructor. May be write to a log, some big red bad text if you can.

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

    0 讨论(0)
  • 2020-11-22 01:03

    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?

    0 讨论(0)
  • 2020-11-22 01:03

    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.

    0 讨论(0)
  • 2020-11-22 01:04

    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)

    0 讨论(0)
  • 2020-11-22 01:06

    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.

    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 Myers 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

    0 讨论(0)
提交回复
热议问题