mr-edd.co.uk :: horsing around with the C++ programming language

fungo: in the absence of std::exception_ptr

[16th October 2010]

During those times when we I'm not able to use a bleeding edge version of Visual C++ of GCC, I occasionally find myself crying out for std::exception_ptr.

For those that are unaware of it, it's a feature that's being added to the latest C++ standard that allows you to store the currently active exception so it can be thrown again, later. This has uses in multi-threaded code, for propagating exceptions across task or thread-join boundaries, and also in code that mixes C and C++[1].

So in the absence of exception_ptr, I tend to make sure that any exception classes I write can be copied and re-thrown polymorphically. This means you can have a single catch clause for the root of the exception hierarchy in order to store the caught exception:

std::auto_ptr<polymorphic_base_exception> caught;

try { task(); }
catch(const polymorphic_base_exception &ex)
{
    caught.reset(ex.clone());
}


// ... and later:
caught->rethrow();

Of course, this is all fine and dandy if you only have one exception hierarchy, but it breaks down when new ones are added, or at the very least it gets more complicated.

Even across my own libraries, I don't have a single shared polymorphic base class for all my exceptions. imagexx has one, pexl has another, as does nanohook, and so on.

I could of course retrofit my own libraries, but I'd still be stuck when dealing with exceptions from other code. So, over the last few days I've had a go at making something that will work with all libraries that advertise this kind of polymorphic exception interface.

An overview of fungo

fungo is the result of this effort. Now this kind of thing is never going to be perfect, mainly because it's just not possible to determine the exact type of an exception caught by a catch(...) clause in C++98[2]. So fungo requires you to register the exceptions you're interested in upfront.

So as an example, to register the standard library exceptions, you'd do something like this:

using namespace std;

fungo::catcher grippy;
grippy.learn_by_example<logic_error>(logic_error("2+2=5"));
grippy.learn_by_example<exception>();
grippy.learn_by_example<runtime_error>(runtime_error("oops"));
// ...

The name learn_by_example is perhaps somewhat curious, so I'll explain it a little. fungo tries its best to make sure that the attempts made to catch the registered exceptions are ordered in the most appropriate fashion. For example, if fungo were to do this, it would be no good:

try { /* ... */ }
catch (const std::exception &e) { /* ... */ }
catch (const std::runtime_error &e) { /* ... */ }
catch (const std::logic_error &e) { /* ... */ }

If a std::runtime_error was in transit, it would be caught by the catch(const std::exception &) clause. When a copy is made for storage, the parts of the std::runtime_error that exist beyond the std::exception base object would be sliced off.

So during registration, the fungo::catcher orders the exceptions by looking at their size (bigger exceptions must be more derived than smaller ones) and also by seeing which of the existing handlers can catch an example of the exception currently being registered. If an existing handler can catch that example exception, it means the handler being registered needs to go before it in the catch sequence.

To use the fungo::catcher to catch and store exceptions we do this:

fungo::exception_cage cage;

try { /* ... */ }
catch (...) { grippy.store_current_exception(cage); }

And later on if we want to re-throw it:

cage.rethrow();

By default, fungo::catcher::store_current_exception() will allow unrecognised exceptions to escape. Given the kinds of use-cases it was designed for, this behaviour might be undesirable. So there's another overload that ensures any unrecognized exception or any exception that is generated during the copy/store operation is caught and represented by a placeholder exception in the fungo::exception_cage:

try { /* ... */ }
catch (...) { grippy.store_current_exception(cage, std::nothrow); }
//                                                 ^^^^^^^^^^^^

In the case of an unrecognized exception being thrown, a fungo::unknown_exception is placed inside the exception_cage. If the clone operation fails, a fungo::clone_failure is stored inside the exception_cage.

The fungo::unknown_exception and fungo::clone_failure classes are derived from std::exception and std::bad_alloc, respectively.

Dealing with polymorphically copyable exception hierarchies

But now let's suppose we have an exception hierarchy rooted in a base class that provides polymorphic copying and re-throwing behaviour — something like imagexx::exception:

namespace imagexx
{
    class exception
    {
        public:
            virtual ~exception();

            std::auto_ptr<exception> clone() const; // calls virtual clone_impl()
            void throw_copy() const; // calls virtual throw_copy_impl()

        private:
            virtual std::auto_ptr<exception> clone_impl() const = 0;
            virtual void throw_copy_impl() const = 0;
    };
}

To register this entire hierarchy, we can specialize the fungo::clone_policy template in order to describe how exceptions caught as imagexx::exception references can be cloned and re-thrown without slicing:

namespace fungo
{
    template<>
    struct clone_policy<::imagexx::exception>
    {
        static ::imagexx::exception *clone(const ::imagexx::exception &ex)
        {
            return ex.clone().release();
        }

        static void rethrow(const ::imagexx::exception &ex)
        {
            ex.throw_copy();
        }
    };
}

Now all we need to do is register imagexx::exception with our fungo::catcher:

fungo::catcher grippy;
grippy.learn<imagexx::exception>();

In this case we use fungo::catcher::learn() rather than learn_by_example() because imagexx::exception has pure virtual functions and so an example exception object can't be made. However, since fungo can see that clone_policy has been specialized for imagexx::exception it will prioritize it over all unspecialized handlers.

An interesting implementation detail

One last thing I'll mention in an attempt to convince you that fungo is probably the best we can do without std::exception_ptr, is to mention a technique I used to reduce the number of allocations needed when copying and storing exceptions.

It's important to minimize allocation because C++'s new operator can of course itself throw an std::bad_alloc, which confuses the situation somewhat; in some rare/unlucky scenario where allocation fails we might end up storing an std::bad_alloc instead of the actual exception that was thrown.

So when storing an exception, only a single allocation is performed by fungo, in order to copy/clone the caught exception for storage. Why do I think this is so neat? Well consider what might happen when you call fungo::learn() or fungo::learn_by_example(). One might imagine that there are a couple of base classes for catching and storing exceptions of different kinds:

class store
{
    public:
        virtual ~store();
        virtual void rethrow() const = 0;
};

class handler
{
    public:
        virtual ~handler();
        virtual std::auto_ptr<store> attempt_catch() const = 0;
        // ...
};

If this kind of setup were used, we'd obviously need to allocate a derived store object to hold the allocated clone, which of course makes two allocations.

To workaround this, the handler class (actually called glove internally) is merely a collection of function pointers that work with void pointers.

So the only state involved during a catch/copy/store operation is a void pointer (that resulted from the cloning the caught exception) and some accompanying function pointers. This means that we don't have to dynamically allocate anything like the store object above.

Conclusion

As I said it's not a perfect solution, but I think this is about as good as we can get without a bona fide std::exception_ptr implementation. If you can see any potential improvements, however, please let me know!

Footnotes
  1. does your implementation make any guarantees about what happens when a C++ exception is thrown from a callback invoked in some C code? []
  2. though if you know of any platform-specific extensions, please let me know! []

Comments

(optional)
(optional)
(required, hint)

Links can be added like [this one -> http://www.mr-edd.co.uk], to my homepage.
Phrases and blocks of code can be enclosed in {{{triple braces}}}.
Any HTML markup will be escaped.