5
\$\begingroup\$

I wanted to implement my own runtime error class in C++, that could provide more meaningful information on where the error occurred than std::runtime_error does.

I decided to use boost::stacktrace to generate a stack trace, appended to the exception message.

Anyway, this is my code:

/**
    \brief Represents a runtime error.

    Equivalent of std::runtime_error, but contains a stacktrace
    generated by Boost.
*/
class Exception : public std::runtime_error
{
    public:
        /**
            \brief Returns error description.

            If the backtrace was generated successfully,
            returns the message along with stack trace.
            Otherwise, behaves like std::runtime_error::what().
        */
        const char *what( ) const noexcept override
        {
            if ( has_backtrace )
                return message.c_str( );
            else
                return std::runtime_error::what( );
        }

        /**
            \brief Constructs an exception, generates a backtrace if possible.

            \param cmessage The error description.
        */
        Exception( const char *cmessage ) :
            std::runtime_error( cmessage ) // std::runtime_error will store the original message
        {
            // Attempt to generate stacktrace
            try
            {
                std::stringstream buffer;
                buffer << cmessage << std::endl << boost::stacktrace::stacktrace( );
                message = buffer.str( );
                has_backtrace = true;
            }
            catch ( std::exception &ex )
            {
                has_backtrace = false;
            }
        }

    protected:
        bool has_backtrace; //!< Determines if backtrace is present
        std::string message; //!< Contains the user message along with backtrace.
};

I can tell it works on this simple example

void foo( )
{
    throw Exception( "some message" );
}

void bar( )
{
    foo( );
}

int main( )
{
    try
    {
        bar( );
    }
    catch ( std::exception &ex )
    {
        std::cerr << ex.what( );
    }

    return 0;
}

because it generates following message:

some message
 0# Exception::Exception(char const*) in ./ex
 1# foo() in ./ex
 2# bar() in ./ex
 3# main in ./ex
 4# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 5# _start in ./ex

However, what I'd like to know is if my code isn't too complicated as for an exception class and if it properly handles possible stack trace generator exceptions and properly falls back to std::runtime_error functionality.

Also, is there some important property/method that all exception classes should have, that I had overlooked and forgot to implement? Maybe the whole idea of having such class is bad in general and I should use some other solution?

Other suggestions are obviously welcome too.

\$\endgroup\$
5
\$\begingroup\$
Exception( const char *cmessage ) :

I strongly recommend making this constructor explicit, so that you don't permit the user to accidentally write

void some_function(const Exception&);
...
some_function("hello world");

My impression is that most C++ experts recommend explicit on one-argument constructors; I personally hold the minority opinion that explicit should be present on all constructors by default (i.e., if you don't have a specific immediate need for implicit conversion, you should just put explicit automatically, and sort out the consequences later if necessary).


catch ( std::exception &ex )

My current employer's codebase actually does catch by non-const reference, but if you're starting from scratch, I strongly recommend catching by const reference by default. Accidentally mutating the current exception object can cause race conditions.


Also, in the Exception constructor, your catch (std::exception&) should almost certainly be catch (...), right?


As for the high-level point of what you're doing, I believe I have an outline of this sort of thing under "Mix-Ins" somewhere near the end of Template Normal Programming. (Sorry, I'm rushed right now, but I can come back and give you a real link later.) (LATER: I elided that part of the talk, but the two slides on mixins are visible very briefly here.)

The first improvement is to make your thing a template inheriting from T instead of from std::runtime_error specifically. Second, I'd make a function my::throw_with_stacktrace(T&& ex) that simply does throw Exception<std::remove_reference_t<T>>(std::forward<T>(ex));.

Gotta run, but I hope this helps a bit.

\$\endgroup\$
  • 1
    \$\begingroup\$ Thanks for quick response. I'm relatively new to C++ (I've been using pure C extensively before), so I figured out that the best way to learn is by starting some C++ project and facing the real issues in practice. I really appreciate the learning resources. I believe the rvalue references and more advanced template stuff are something that I definitely need to dive more into, because I don't really get them. Your answer is really helpful to me, because now I see an actual use for these concepts. Again, thank you for help. \$\endgroup\$ – Jacajack Sep 16 '18 at 17:21
4
\$\begingroup\$
  1. It's very surprising that your custom exception-class can throw an exception for anything but original creation. std::exception and derived standard classes aren't allowed to do that.

  2. Is it actually significant whether the object stores a stack-trace at all? Most likely not. So, consider simply modifying the passed message instead of creating your own classes. That calls for a (potentially templated) function instead.

  3. As-is, you store the original message, and then try to store the enhanced message and a flag that the enhanced message exists (which is equivalent to not being empty, so why a flag???) and should be used instead. That's a waste.

\$\endgroup\$
  • \$\begingroup\$ I'm sorry, but I don't think I understand the 1st point. Do you mean the lack of noexcept specifier alongside the constructors and assignment operator? If not, could you elaborate, please? \$\endgroup\$ – Jacajack Sep 16 '18 at 17:36
  • 1
    \$\begingroup\$ I mean that the copy- ctor/assignment can throw, as the std::string-member may need to allocate memory. But do you really want to upgrade it from throw std::bad_alloc; to std::terminate(), instead of going the COW-route? \$\endgroup\$ – Deduplicator Sep 16 '18 at 17:54
  • \$\begingroup\$ Alright, I see. I didn't think of that. As you said, the dedicated throw function seems like a much better solution now. Thanks for help. \$\endgroup\$ – Jacajack Sep 16 '18 at 18:02
  • \$\begingroup\$ I like the dedicate function idea rather than creating a class. BUT then I hate functions creating my exception objects so I am bit worried about that. Why not convert it to a message object that is passed to the constructor. That converts itself to a string for the constructor. \$\endgroup\$ – Martin York Sep 18 '18 at 14:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.