Friday, December 13, 2013

Using a Lippincott Function for Centralized Exception Handling

The lippincott function is a way to wrap the handling of many different exceptions into a single reusable function.

Consider this use case:
  • A C++ library (say, libfoo) has many distinct exception types.
  • You want to wrap the C++ library with a C API.
  • You want error codes instead of exceptions for the C API.

A simple approach is to write every C API function as follows:
typedef enum foo_Result {
    FOO_OK,
    FOO_ERROR1,
    FOO_ERROR2,
    FOO_UNKNOWN
} foo_Result;

foo_Result foo_dothing()
{
    try
    {
        // Can throw MyException1 or MyException2
        foo::DoThing();
    }
    catch (const MyException1&)
    {
        return FOO_ERROR1;
    }
    catch (const MyException2&)
    {
        return FOO_ERROR2;
    }
    catch (...)
    {
        return FOO_UNKNOWN;
    }

    return FOO_OK;
}
There are some maintenance problems with the above approach:
  • DoThing() might later throw more exception types.
  • You must repeat the exception handling code for every API function.

Jon Kalb suggests the following refactoring method, which he named after Lisa Lippincott, who taught him the technique.
foo_Result lippincott()
{
    try
    {
        throw;
    }
    catch (const MyException1&)
    {
        return FOO_ERROR1;
    }
    catch (const MyException2&)
    {
        return FOO_ERROR2;
    }
    catch (...)
    {
        return FOO_UNKNOWN;
    }
}

foo_Result foo_dothing()
{
    try
    {
        foo::DoThing();
        return FOO_OK;
    }
    catch (...)
    {
        return lippincott();
    }
}
"throw;", when inside a catch block, simply rethrows the currently caught exception. In this case, the "throw;" is not directly placed in a catch block within the lippincott() function, but it is transitively (safely) called from within the catch block of foo_dothing().

There are some important preconditions to calling lippincott():
  • You cannot call the lippincott function from outside a catch block.
    • "throw;" outside of a catch block will call std::terminate().
  •  lippincott() must be noexcept. No exceptions should escape it.
    • The exception will leak out of the C API otherwise.

If we want extra safety, we can implement safeguards for the aforementioned preconditions:

To safely handle a violation of the first precondition, we can check that std::current_exception()  is not null previous to doing the "throw;"

To prevent an exception from being thrown out of lippincott(), we can wrap the whole body of the function in a try/catch.

Here is the "extra safe"/paranoid version of lippincott():
foo_Result lippincott()
try
{
    try
    {
        if (std::exception_ptr eptr = std::current_exception())
        {
            std::rethrow_exception(eptr);
        }
        else
        {
            return FOO_UNKNOWN;
        }
    }
    catch (const MyException1&)
    {
        return FOO_ERROR1;
    }
    catch (const MyException2&)
    {
        return FOO_ERROR2;
    }
    catch (...)
    {
        return FOO_UNKNOWN;
    }
}
catch (...)
{
    return FOO_UNKNOWN;
}
The C API can now be written entirely in the style of foo_dothing(), which will centralize the error code conversion through the lippincott function.

Another interesting idea is to use the lippincott function to convert exceptions into string representations for debugging. This area is especially in need of the extra try/catch surrounding the function, since allocating dynamic strings can fail.

Full working example: http://ideone.com/m2ZfHN

The term "lippincott function" was popularized by Jon Kalb in his exception-safe coding talks.
You can find his explanation here: youtube link

1 comment:

  1. comments: http://www.reddit.com/r/programming/comments/1ss9j2/c_secrets_using_a_lippincott_function_for/

    ReplyDelete