Sunday, July 14, 2013

Exception Neutral vs. Exception Safe

The last entry I made was all about throw specifiers. Now I want to cover exception safety vs. exception neutral; the two main "degrees" of exception safety guarantees. The book Exceptional C++ has a (pardon the expression) exceptional explanation of these two levels- but not enough clarification in the examples. I thought I'd take the time to present some code examples that illustrate exception safe and exception neutral guarantees. 

Exception Neutrality

From what I understand (and I'll cite this), all exception neutrality is means that if you aren't handling an exception allow the exception to propagate [2]. So the following example would NOT be exception neutral:

void exceptionNonNeutral()
{
    try 
    {
         functionThatCouldThrow();
    }
    catch(...)
    {
        // Eat all exceptions thrown!
    }
}

I used to be a fan of eating all exceptions and throwing them away- however this doesn't allow exceptions to be handled by callers further down the stack. It can hide problems that may exist in functionThatCouldThrow(). To make this function exception neutral, might I suggest an extra throw?

void exceptionNeutral()
{
    try 
    {
         functionThatCouldThrow();
    }
    catch(...)
    {
        // Adding this line allows us to re-throw whatever was
        // thrown earlier. This is the key for exception 
        // neutrality.
        throw;
    }
}

The funny thing is that if you never added a try-catch block (i.e. no exceptions) you are automatically exception neutral because the exception would unwind the stack. It's only when you start catching exceptions that you need to worry about being neutral to exceptions that aren't understood by your function.

Exception Safe

This one is a bit harder to determine (and this is where Sutter scared me in Exceptional C++). Any function could throw an exception- which should make developers paranoid. This is also why writing exception safe code is so difficult as every single function could throw something and we have no idea what that might be! The only thing we can do as sane rational people is take care of our own code and safeguard it in case an exception is thrown.

The following is not exception safe:

#include <iostream>
#include <stdexcept>

struct A
{
    A(bool throwSomething) 
    { 
        if (throwSomething)
        {
            throw std::runtime_error("Exception!");
        }
    }

    int magicNumber() const { return 13; }

};

static A* internalObject = 0;

void reset()
{
     delete internalObject;
     internalObject = new A(true);
}

int getMagicNumber()
{
    return (0 != internalObject ? internalObject->magicNumber() : 0);
}

Let's cover what is going on. I've created a structure A which may or may not throw based on the bool passed in. The purpose is to illustrate exception safety- in the real world this could be a call to new that throws, etc. For the sake of simplicity, we'll keep it simple. 

The function reset() is not exception safe because it leaves the program in an inconsistent state. In a normal situation, getMagicNumber() would return a number with no incidents whether or not reset() was called. If getMagicNumber() calls without a valid internalObject it will return 0. However, if internalObject is non-zero and points at memory it will return the contents of the magicNumber() method.

Here's what can go wrong and why we should all be paranoid, if during reset() we invoke the constructor for A and it throws then the internalObject has already been deleted. That's really bad! The internalObject was destroyed, yet the internalObject pointer value is still non-zero. Then our user calls getMagicNumber() which  states that yes, internalObject is non-zero (but pointing at destroyed memory) and therefore we're going to call magicNumber() on that garbage memory. 

Exception safety means we have to keep the state of the program intact. Therefore we could do the following to fix this program:

#include <iostream>
#include <stdexcept>

struct A
{
    A(bool throwSomething) 
    { 
        if (throwSomething)
        {
            throw std::runtime_error("Exception!");
        }
    }

    int magicNumber() const { return 13; }

};

static A* internalObject = 0;

void reset()
{
    // Store a pointer to the old object.
    A* old = internalObject;

    // Attempt to create a new A object, if this throws
    // then internalObject is never set.
    internalObject = new A(true);

    // Delete the old object.
    delete old;
}

int getMagicNumber()
{
    return (0 != internalObject ? internalObject->magicNumber() : 0);
}

So here's the moral of the story, to make functions exception safe sometimes it's just a matter of ordering. In the fix above all that was done is to move the delete to the end. Delete operations should always be non-throwing [3]- that's the rule in C++. There's another operation that should also be non-throwing and that is "swap". A non-throwing swap is a good foundation to insuring exception safe code. The fix above is very close to using a swap operation. 



[3] - Exceptional C++, Item 16 - Herb Sutter

No comments:

Post a Comment