Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ exception handling and error reporting idioms

In C++, RAII is often advocated as a superior approach to exception handling: if an exception is thrown, the stack is unwound, all the destructors are called and resources are cleaned up.

However, this presents a problem with error reporting. Say a very generic function fails, the stack is unwound to the top level and all I see in the logs would be:

Couldn't read from socket: connection reset by peer.

...or any equally generic message. This doesn't say much about the context from which the exception is thrown. Especially if I'm running something like an event queue processing loop.

Of course I could wrap every call to socket reads with a try/catch block, catch the exception, construct a new one with more detailed context information and re-throw it, but it defeats the purpose of having RAII, and is slowly but surely becoming worse than handling return error codes.

What's a better way for detailed for error reporting in standard C++? I'm also open to suggestions involving Boost.

like image 291
Alex B Avatar asked Sep 24 '10 00:09

Alex B


2 Answers

As James McNellis suggested here, there is a really neat trick involving a guard object and the std::uncaught_exception facility.

The idea is to write code like this:

void function(int a, int b)
{
  STACK_TRACE("function") << "a: " << a << ", b: " << b;

  // do anything

}

And have the message logged only in case an exception is actually thrown.

The class is very simple:

class StackTrace: boost::noncopyable // doesn't make sense to copy it
{
public:
  StackTrace(): mStream() {}

  ~StackTrace()
  {
    if (std::uncaught_exception())
    {
      std::cout << mStream.str() << '\n';
    }
  }

  std::ostream& set(char const* function, char const* file, unsigned int line)
  {
    return mStream << file << "#" << line << " - " << function << " - ";
  }

private:
  std::ostringstream mStream;
};

#define STACK_TRACE(func)                           \
  StackTrace ReallyUnwieldyName;                    \
  ReallyUnwieldyName.set(func, __FILE__, __LINE__)

One can use __PRETTY_FUNC__ or equivalent to avoid naming the function, but I have found in practice that it was too mangled / verbose for my own taste.

Note that you need a named object if you wish it to live till the end of the scope, which is the purpose here. We could think of tricky ways to generate a unique identifier, but I have never needed it, even when protecting narrower scope within a function, the rules of name hiding play in our favor.

If you combine this with an ExceptionManager (something where exceptions thrown register themselves), then you can get a reference to the latest exception and in case of logging you can rather decide to setup your stack within the exception itself. So that it's printed by what and ignored if the exception is discarded.

This is a matter of taste.

Note that in the presence of ExceptionManager you must remain aware that not all exceptions can be retrieved with it --> only the ones you have crafted yourself. As such you still need a measure of protection against std::out_of_range and 3rd party exceptions.

like image 190
Matthieu M. Avatar answered Oct 13 '22 01:10

Matthieu M.


Say a very generic function fails, the stack is unwound to the top level and all I see in the logs would be [...]
What's a better way for detailed for error reporting in standard C++?

Error handling isn't local to a class or library - it is a application level concern.

Best I can answer you question is that the error reporting should be always implemented by looking first and foremost at the error handling. (And the error handling also including the handling of the error by the user.) Error handling is the decision making about what has to be done about the error.

That is one of the reasons why error reporting is an application level concern and strongly depends on the application workflow. In one application the "connection reset by peer" is a fatal error - in another it is a norm of life, error should be silently handled, connection should be reestablished and pending operation retried.

Thus the approach you mention - catch the exception, construct a new one with more detailed context information and re-throw it - is a valid one too: it is up to the top level application logic (or even user configuration) to decide whether the error is really an error or some special (re)action has to taken upon the condition.

What you have encountered is one of the weaknesses of so called out-of-line error handling (aka exceptions). And I'm not aware of any way to do it better. Exceptions create a secondary code path in the application and if error reporting is vital the design of the secondary code path has to treated just like the main code path.

Obvious alternative to the out-of-line error handling is the in-line error handling - good ol' return codes and log messages on the error conditions. That allows for a trick where application saves all low-severity log messages into internal (circular) buffer (of fixed or configurable size) and dumps them into the log only if high-severity error happens. This way more context information is available and different layers of application do not have to be actively aware of each other. That is also standard (and sometimes literally "standard" - mandated by law) way of error reporting in application fields like safety and mission critical software, were one is not allowed to miss errors.

like image 30
Dummy00001 Avatar answered Oct 13 '22 02:10

Dummy00001