Unexpected skip of a destructor

Writing fairly straightforward C++ code, we usually make heavy use of the RAII concept. Therefore, we greatly rely on the simple (and basic) assumption that all appropriate destructors will be called. What happens if that is not the case?

Consider the following piece of code:

#include <iostream>
#include <stdexcept>

struct A {
    A () { std::cout << "A" << std::endl; }
    ~A () { std::cout << "~A" << std::endl; }
};

void myfunc () {
    A temp;
    throw std::runtime_error("moo");
}

int main () {
    myfunc();
}

You would probably imagine that there is absolutely nothing special about this code, and that both the constructor and the destructor should be called to produce their outputs. However, that is not what actually happens.

The given program produces this output (at least with my XCode):

A
terminate called throwing an exception

The most important detail to take note of in this example, is that the output of A’s destructor is nowhere to be found – the destructor call is missing. What if that destructor was supposed to free a lock or a mutex, or worse – some global system resource? That could be nothing short of a disaster.

Digging further, adding a catch clause to the outermost code would solve the issue:

int main () {
    try {
        myfunc();
    }
    catch (...) {
        // do nothing
    }
}

However, I find the reasoning for this phenomena to be the interesting part of the story. Looking at the C++ standard draft, section 15.3 [except.handle] point 9 reads:

If no matching handler is found in a program, the function terminate()
(_except.terminate_) is called. Whether or not the stack is unwound
before calling terminate() is implementation-defined.

So what happens here is that since the original program never attempts to catch the given exception, unexpected() ends up getting called. Reading the given section of the C++ standard lets us know that stack unwinding is not mandatory in this case, and as such – many compilers are likely to skip implementing it. As far as I know, both gcc and Visual Studio behave the same in this context, and do not perform a full stack unwinding in this case.

Moreover, a bug of this nature actually happened to a colleague of mine: essential destructor code was missing from actual production execution, causing a leak of global system resources. That was a hard bug to track.

8 thoughts on “Unexpected skip of a destructor

  1. Awesome, thanks for that lesson!

    While I don’t recall ever running into this situation (skipped destructors having critical system side effects), I’m sure I came close! It certainly would have been a hard bug to track down.

    Curious how different the case is when the exception is thrown from another std::thread.. I would guess there is a default handler which moves the exception across thread boundary and so the stack would unwind at least up to thread boundary..

    Thanks for sharing!

    1. Quoting N3290,

      15.3 para 6:
      “If no match is found among the handlers for a try block, the search for a matching handler continues in a
      dynamically surrounding try block of the same thread.”

      15.3 para 9:
      “If no matching handler is found, the function std::terminate() is called; whether or not the stack is
      unwound before this call to std::terminate() is implementation-defined (15.5.1).”

      The way I read this is that the unhandled exception from a thread causes the termination of the entire program, and the thread’s stack may not be unwound. Interestingly, it says nothing about other threads’ stacks.

  2. I’m guessing the rationale behind this standard behaviour is – if an exception goes uncaught the process will terminate, and we might as well leave the due cleanup to the OS process-disposal facilities. At least in windows, the OS does release on your behalf all resources (including global ones). You could shoot yourslef in the foot with some exotic behaviour (http://blogs.msdn.com/b/oldnewthing/archive/2007/05/03/2383346.aspx), but if you rely only on standard resources and don’t try anything fancy (as in thread creation) during dll-detach, that rationale should hold. What resources exactly were leaked in your colleague’s case? Was it windows?

    1. Actually, the problem in our case was not resource leakage (although that can certainly happen with resources that the kernel will not free for you – especially when dealing with custom drivers and the likes, or when only a part of the process dies), but was actually quite funny:
      - We had a perfectly functioning code to begin with, which stopped working as soon as a new catch clause has been added somewhere along its call chain. It actually started causing segmentation faults.
      - With some debug we have pinpointed the problem to be related with the destruction order of certain dependant objects. It turned out that a specific destructor which was not called until now was actually attempting to access an (already) unavailable resource, resulting in a segfault.

      All in all, the behaviour described in the post has just unveiled a dormant bug in our case, but making the connection between a new catch-clause and some destructor was not trivial.

      And yeah, it was Linux.

  3. I’ve come across this. It’s pretty annoying if you want to print out your error code if the exception is not caught.
    My solution was overriding std::terminate and print all exceptions which still existed.

  4. Interesting article! Let’s consider this:

    #include <iostream>
    #include <stdexcept>
    
    struct A {
        A () { std::cout << "A" << std::endl; }
        ~A () { std::cout << "~A" << std::endl; }
    };
    
    void myfunc1 () try {
        A temp;
        throw std::runtime_error("meow");
    } catch(...)
    {
        throw;
    }
    
    int main () {
        myfunc1();
    }
    

    Output for g++ (GCC) 4.2.3 is:

    A
    ~A
    terminate called after throwing an instance of 'std::runtime_error'
    what(): meow
    Aborted

    It’s called function-try-block (or similar) and ensures you that destructors of every on-stack object will be called (curly braces for try hold scope). To propagate caught exception one may rethrow it as it is done in the example above. I think this is pretty nice technique to prevent destructor skip disaster described by you.

  5. These seems like such a simple example to fail. If RAII can’t free you of try/catch blocks, is there any point to even talking about when it comes to exceptions?

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>