Proper/elegant way of implementing C++ exception chaining?
Asked Answered
c++
M

4

17

I'd like to implement an Exception class in C++ that mimics the one from .NET framework (and Java has something similar too), for the following purposes:

  1. Exception chaining: I'd like to implement the concept of "exception translation", when exceptions caught at higher levels wrap and "translate" the lower level exceptions, also preserving these lower-level exceptions somehow (in the InnerException member, in this case). For this, there should be some mechanism to store inner exceptions along with each exception thrown at the upper level. InnerException member provides this in the implementation below.

  2. Exception inheritance: there should be possible to derive IoException from Exception, and SerialPortException from IoException, for example. While this seems trivial, there should be ability to identify the type of caught exceptions dynamically (e.g. for logging purposes, or to display to user), preferably without the overhead of RTTI and typeid.

This is the sample exception handling logic I'd like to make possible:

try
{
    try
    {
        try
        {
            throw ThirdException(L"this should be ThirdException");
        }
        catch(Exception &ex)
        {
            throw SubException(L"this should be SubException", ex);
        }
    }
    catch(Exception &ex)
    {
        throw SubException(L"this should be SubException again", ex);
    }
}
catch(Exception &ex)
{
    throw Exception(L"and this should be Exception", ex);
}

and when catching the "outer-most" exception in the upmost layer I'd like to be able to parse and format whole exception chain through the InnerException member, to display something like this:

Exception chain formatting

I've come up with the following implementation so far:

Small note: CString is Microsoft-specific string class (just for the people not familiar with Visual C++ stuff).

class Exception
{
protected:

    Exception(const Exception&) {};
    Exception& operator= (const Exception&) {};

public:

    Exception(const CString &message) : InnerException(0), Message(message) {}
    Exception(const CString &message, const Exception &innerException) : InnerException(innerException.Clone()), Message(message) {}

    virtual CString GetExceptionName() const { return L"Exception"; }

    virtual Exception *Clone() const
    {
        Exception *ex = new Exception(this->Message);
        ex->InnerException = this->InnerException ? this->InnerException->Clone() : 0;
        return ex;
    }

public:

    virtual ~Exception() { if (InnerException) delete InnerException; }

    CString Message;
    const Exception *InnerException;
};

Now what do we have here. Copy constructor and assignment operator are made protected to prevent copying. Each object will "own" its inner exception object (and delete it in destructor), so default shallow-copying would be unacceptable. Then we have two pretty standard-looking constructors and virtual destructor that deletes the InnerException object. Clone() virtual method is responsible for deep-copying the objects, primarily for storing the inner exception object (see the second constructor). And finally GetExceptionName() virtual method provides the cheap alternative to RTTI for identification of exception class names (I don't think this looks cool but I couldn't come up with better solution; for comparison: in .NET one could simply use someException.GetType().Name).

Now this does the job. But... I don't like this solution for one particular reason: the amount of coding needed for each derived class. Consider I have to derive SubException class, which provides absolutely zero additions to the base class functionality, it just provides the custom name ("SubException", which might be "IoException", "ProjectException", ...) to differentiate it for its usage scenario. I have to provide almost same amount of code for each of such exception class. Here it is:

class SubException : public Exception
{
protected:

    SubException(const SubException& source) : Exception(source) {};
    SubException& operator= (const SubException&) {};

public:

    SubException(const CString &message) : Exception(message) {};
    SubException(const CString &message, const Exception &innerException) : Exception(message, innerException) {};

    virtual CString GetExceptionName() const { return L"SubException"; }

    virtual Exception *Clone() const
    {
        SubException *ex = new SubException(this->Message);
        ex->InnerException = this->InnerException ? this->InnerException->Clone() : 0;
        return ex;
    }
};

I don't like the fact that I have to provide protected copy constructor and assignment operator each time, I don't like the fact that I have to clone the Clone method each time, duplicating even the code of copying the base members (InnerException...), simply... I don't think this is the elegant solution. But I was unable to think of better one. Do you have any ideas how to implement this concept "properly"? Or maybe this is the best implementation of this concept that is possible in C++? Or maybe I'm doing this completely wrong?

P.S.: I know there exist some mechanisms in C++11 (also in Boost) for this purpose (exception chaining) with some new exception classes, but I'm primarily interested in custom, "old-C++-compatible" ways. But it would be good, in addition, if someone could provide any code in C++11 that accomplishes the same.

Mccowan answered 13/11, 2012 at 7:53 Comment(4)
This sounds like Boost.Exception.Munn
Maybe it does, but I can't use Boost for what I need this for. As I mentioned, it is probably also directly possible in C++11, but I'm searching for the custom alternative solution, not dependent on Boost, or C++11, or anything...Mccowan
You can you boost, or at least some headers like this out of it. Boost has some complex components, but the utility modules like exception can be easily copied out of it and included in your project. Most of boost, including exception, compiles just fine under Visual C++ including old WinCE targets like Pocket PC 2003 (ARMV4).Aureus
See also https://mcmap.net/q/744529/-should-exceptions-be-chained-in-c-duplicate/545127Adjunction
L
21

C++11 already has nested_exception. There was a talk about exceptions in C++03 and C++11 at Boostcon/C++Next 2012. Videos are on youtube:

  1. http://www.youtube.com/watch?v=N9bR0ztmmEQ&feature=plcp
  2. http://www.youtube.com/watch?v=UiZfODgB-Oc&feature=plcp
Lemberg answered 13/11, 2012 at 8:4 Comment(0)
U
4

There is a lot of extra code, but the good thing is it's really EASY extra code that doesn't change at all from class to class, so it's possible to preprocessor macro it.

#define SUB_EXCEPTION(ClassName, BaseName) \
  class ClassName : public BaseName\
  {\
  protected:\
  \
      ClassName(const ClassName& source) : BaseName(source) {};\
      ClassName& operator= (const ClassName&) {};\
  \
  public:\
  \
      ClassName(const CString &message) : BaseName(message) {};\
      ClassName(const CString &message, const BaseName &innerException) : BaseName(message, innerException) {};\
  \
      virtual CString GetExceptionName() const { return L"ClassName"; }\
  \
      virtual BaseName *Clone() const\
      {\
          ClassName *ex = new ClassName(this->Message);\
          ex->InnerException = this->InnerException ? this->InnerException->Clone() : 0;\
          return ex;\
      }\
  };

Then you can define various utility exceptions by just doing:

SUB_EXCEPTION(IoException, Exception);
SUB_EXCEPTION(SerialPortException, IoException);
Ungotten answered 13/11, 2012 at 8:5 Comment(1)
@Mccowan Microsoft and Boost do use Macros fairly extensively - you should take a look at Boost Fusion for some Macro Fun ;) boost.org/doc/libs/1_52_0/libs/fusion/doc/html/index.htmlCathey
S
2

Few years ago I wrote this: Unchaining Chained Exceptions in C++

Basically, the exceptions are not nested inside each other, because it would be difficult to catch the original one, but another mechanism keeps track of all the functions visited by the exception while it travels to its catch point.

A revisited version of that can be found in the library Imebra on Bitbucket, here and here.

Now I would rewrite that with some improvements (e.g. use local thread storage to keep the stack trace).

Using this approach allows you to catch the original exception that was thrown, but to still have the stack trace and possibly other information added by the functions visited by the exception while it travels back to the catch statement.

Sapid answered 15/11, 2012 at 8:10 Comment(0)
R
2

Please don't follow boost::exception approach. Boost::exception is for different use case - in particular it's usefull when you want to collect precise exception context scatered over call stack. Consider the following example:

#include "TSTException.hpp"

struct DerivedException: TST::Exception {};

int main() try
{
    try
    {
        try
        {
            try
            {
                throw std::runtime_error("initial exception");
            }
            catch(...)
            {
                throw TST::Exception("chaining without context info");
            }
        }
        catch(...)
        {
            TST_THROW("hello world" << '!');
        }
    }
    catch(...)
    {
        TST_THROW_EX(DerivedException, "another exception");
    }
}
catch(const TST::Exception& ex)
{
    cout << "diagnostics():\n" << ex;
}
catch(const std::exception& ex)
{
    cout << "what(): " << ex.what() << endl;
}

The "exception chaining" solution as I understand it should produce output similar to this:

$ ./test
diagnostics():
Exception: another exception raised from [function: int main() at main.cpp:220]
Exception: hello world! raised from [function: int main() at main.cpp:215]
Exception: chaining without context info raised from [function: unknown_function at unknown_file:0]
Exception: initial exception

As you see there are exceptions chained to each other and diagnostic output contains all exceptions with context information and optional stack trace (not shown here, because it's compiler/platform dependent). "Exception chaining" can be naturally achieved using new C++11 error handling features (std::current_exception or std::nested_exception). Here is implementation of TSTException.hpp (please bear with more source code):

#include <iostream>
#include <sstream>
#include <stdexcept>
#include <exception>
#include <vector>
#include <string>
#include <memory>
#include <boost/current_function.hpp>
#include <boost/foreach.hpp>

using namespace std;

namespace TST
{

class Exception: virtual public std::exception
{
public:
    class Context
    {
    public:
        Context():
            file_("unknown_file"),
            line_(0),
            function_("unknown_function")
        {}
        Context(const char* file, int line, const char* function):
            file_(file? file: "unknown_file"),
            line_(line),
            function_(function? function: "unknown_function")
        {}
        const char* file() const { return file_; }
        int line() const { return line_; }
        const char* function() const { return function_; }
    private:
        const char* file_;
        int line_;
        const char* function_;
    };
    typedef std::vector<std::string> Stacktrace;
    //...
    Exception()
    {
        initStacktraceAndNestedException();
    }
    explicit Exception(const std::string& message, const Context&& context = Context()):
        message_(message),
        context_(context)
    {
        message.c_str();
        initStacktraceAndNestedException();
    }
    ~Exception() throw() {}
    //...
    void setContext(const Context& context) { context_ = context; }
    void setMessage(const std::string& message) { (message_ = message).c_str(); }
    const char* what() const throw () { return message_.c_str(); }
    void diagnostics(std::ostream& os) const;
protected:
    const Context& context() const { return context_; }
    const std::exception_ptr& nested() const { return nested_; }
    const std::shared_ptr<Stacktrace>& stacktrace() const { return stacktrace_; }
    const std::string& message() const { return message_; }
private:
    void initStacktraceAndNestedException();
    void printStacktrace(std::ostream& os) const;
    std::string message_;
    Context context_;
    std::shared_ptr<Stacktrace> stacktrace_;
    std::exception_ptr nested_;
};

std::ostream& operator<<(std::ostream& os, const Exception& ex)
{
    ex.diagnostics(os);
    return os;
}

std::ostream& operator<<(std::ostream& os, const Exception::Context& context)
{
    return os << "[function: " << context.function()
              << " at " << context.file() << ':' << context.line() << ']';
}

void Exception::diagnostics(std::ostream& os) const
{
    os << "Exception: " << what() << " raised from " << context_ << '\n';
    if (const bool haveNestedException = nested_ != std::exception_ptr())
    {
        try
        {
            std::rethrow_exception(nested_);
        }
        catch(const TST::Exception& ex)
        {
            if(stacktrace_ && !ex.stacktrace())//if nested exception doesn't have stacktrace then we print what we have here
                    printStacktrace(os);
            os << ex;
        }
        catch(const std::exception& ex)
        {
            if(stacktrace_)
                printStacktrace(os);
            os << "Exception: " << ex.what() << '\n';
        }
        catch(...)
        {
            if(stacktrace_)
                printStacktrace(os);
            os << "Unknown exception\n";
        }
    }
    else if(stacktrace_)
    {
        printStacktrace(os);
    }
}

void Exception::printStacktrace(std::ostream& os) const
{
    if(!stacktrace_)
    {
        os << "No stack trace\n";
        return;
    }
    os << "Stack trace:";
    BOOST_FOREACH(const auto& frame, *stacktrace_)
    {
        os << '\n' << frame;
    }
    os << '\n';
}

void Exception::initStacktraceAndNestedException()
{
    nested_ = std::current_exception();
    if(const bool haveNestedException = nested_ != std::exception_ptr())
    {
        try
        {
            throw;
        }
        catch(const TST::Exception& ex)
        {
            if(ex.stacktrace())
            {
                stacktrace_ = ex.stacktrace();
                return;
            }
        }
        catch(...) {}
    }
    /*TODO: setStacktrace(...); */
}

}//namespace TST

#ifdef TST_THROW_EX_WITH_CONTEXT
#error "TST_THROW_EX_WITH_CONTEXT is already defined. Consider changing its name"
#endif /*TST_THROW_EX_WITH_CONTEXT*/

#define TST_THROW_EX_WITH_CONTEXT(                                      \
    CTX_FILE, CTX_LINE, CTX_FUNCTION, EXCEPTION, MESSAGE)               \
    do                                                                  \
    {                                                                   \
        EXCEPTION newEx;                                                \
        {                                                               \
            std::ostringstream strm;                                    \
            strm << MESSAGE;                                            \
            newEx.setMessage(strm.str());                               \
        }                                                               \
        newEx.setContext(                                               \
            TST::Exception::Context(                                    \
                CTX_FILE, CTX_LINE, CTX_FUNCTION));                     \
        throw newEx;                                                    \
    }                                                                   \
    while(0)

#ifdef TST_THROW_EX
#error "TST_THROW_EX is already defined. Consider changing its name"
#endif /*TST_THROW_EX*/

#define TST_THROW_EX(EXCEPTION, MESSAGE)                                       \
    TST_THROW_EX_WITH_CONTEXT(__FILE__, __LINE__, BOOST_CURRENT_FUNCTION, EXCEPTION, MESSAGE)

#ifdef TST_THROW
#error "TST_THROW is already defined. Consider changing its name"
#endif /*TST_THROW*/

#define TST_THROW(MESSAGE)                                              \
    TST_THROW_EX(TST::Exception, MESSAGE)

I use compiler with partial C++11 support (gcc 4.4.7) so you can see some old style peaces of code here. Just for reference you can use the following compilation parameters to build this example (-rdynamic is for stack trace):

g++ main.cpp TSTException.hpp -rdynamic -o test -std=c++0x

Rotund answered 3/2, 2014 at 6:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.