Retrospective: The simplest error handler

Author: Chloé Lourseyre
Editor: Peter Fordham

This week, we’ll talk about last week’s article and try to be critical about it. There are a few things to say and it occurred to me (thanks to some feedback I got through social media) that it can be improved a lot.

If you don’t know about SimplestErrorHandler, go read the article about it: One of the simplest error handlers ever written | Belay the C++ (belaycpp.com).

The repo containing the feature is still available and is up-to-date with the changes I’ll present here: SenuaChloe/SimplestErrorHandler (github.com).

Version 2: recursion was useless

Indeed.

Sometimes, when you unpack a parameter pack, you may need to differentiate between the main loop of the recursion and the base case. This happens when you need to do more things in the main loop than in the base case.

For the ErrorHandler, the base case does the same thing as the main loop (plus some extras). So we actually don’t need any recursion and can directly unfold the parameter pack into the stream (just like it is done in the concept declaration):

template<typename TExceptionType = BasicException, typename ...TArgs>
requires ErrorHandlerTemplatedTypesConstraints<TExceptionType, TArgs...>
void raise_error(const TArgs & ...args)
{
    std::ostringstream oss;
    (oss << ... << args);
    const std::string error_str = oss.str();
    std::cerr << error_str << std::endl;
    throw TExceptionType(error_str);
}

Avoiding recursion is preferred when you can because recursion accumulates stack frames, which are best to avoid for several reasons1.

Thanks to this new form, we don’t need an auxiliary function (to pass down the std::ostringstream) anymore. Thus, we also don’t need private functions. Without private functions, there is no use for the class, so we can use a namespace instead. And since we now use a namespace, we can declare the concept inside it.

namespace ErrorHandler
{   
    template<typename TExceptionType, typename ...TArgs>
    concept TemplatedTypesConstraints = requires(std::string s, std::ostringstream oss, TArgs... args)
    {
        TExceptionType(s); // TExceptionType must be constructible using a std::string
        (oss << ... << args); // All args must be streamable
    };

    // ...

    template<typename TExceptionType = BasicException, typename ...TArgs>
    requires TemplatedTypesConstraints<TExceptionType, TArgs...>
    void raise_error(const TArgs & ...args)
    {
        // ...
    }

    template<typename TExceptionType = BasicException, typename ...TArgs>
    requires TemplatedTypesConstraints<TExceptionType, TArgs...>
    void assert(bool predicate, const TArgs & ...args)
    {
       // ...
    }
};

Discussion: Need for speed?

String streams are slow. That is a fact2. Plus, in our case, they aren’t especially practical to use (we need to declare a std::ostringstream locally, which means an additional #include, just for that). Is there a way to get rid of that?

The main reason string streams are slow is to-string conversions. However, to keep it as simple as we can (in our ErrorHandler), we want to let the std::ostringstream do its magic, even if it means slower-code.

Time performance is rarely critical (we can safely say that 80% of the time it is not critical). What we are developing is an error thrower. The only reason an error thrower would be inside a performance-sensitive piece of code is if we use it as control flow.

But that would be a mistake. It is called ErrorHandler after all, not FlowControlHandler. By design, it is not to be used in critical code. The only good way to use it in critical code is to get out of it in case of error.

So no, we won’t try to optimize the handler time-wise. We will keep it simple and short. We don’t need speed.

The full code

Here is the last version of the error handler:

#pragma once

#include <iostream>
#include <sstream>

namespace ErrorHandler
{   
    template<typename TExceptionType, typename ...TArgs>
    concept TemplatedTypesConstraints = requires(std::string s, std::ostringstream oss, TArgs... args)
    {
        TExceptionType(s); // TExceptionType must be constructible using a std::string
        (oss << ... << args); // All args must be streamable
    };

    class BasicException : public std::exception
    {
    protected:
        std::string m_what;
    public:
        BasicException(const std::string & what): m_what(what) {}
        BasicException(std::string && what): m_what(std::forward<std::string>(what)) {}
        const char * what() const noexcept override { return m_what.c_str(); };
    };

    template<typename TExceptionType = BasicException, typename ...TArgs>
    requires TemplatedTypesConstraints<TExceptionType, TArgs...>
    void raise_error(const TArgs & ...args)
    {
        std::ostringstream oss;
        (oss << ... << args);
        const std::string error_str = oss.str();
        std::cerr << error_str << std::endl;
        throw TExceptionType(error_str);
    }

    template<typename TExceptionType = BasicException, typename ...TArgs>
    requires TemplatedTypesConstraints<TExceptionType, TArgs...>
    void assert(bool predicate, const TArgs & ...args)
    {
        if (!predicate)
            raise_error<TExceptionType>(args...);
    }
};

Wrapping up

Version 2 of the ErrorHandler is even shorter and simpler than the first. This is a major improvement.

I want to give special thanks to the people who noticed that mistakes were made and suggested improvement.

Thanks for reading and see you next week!

Author: Chloé Lourseyre
Editor: Peter Fordham

Addenda

Github repo

SenuaChloe/SimplestErrorHandler (github.com)

Notes

  1. Mainly to avoid stack overflow and make debugging more readable. At the end of the day, recursion is not really bad, especially in a case like this where stack overflow is unlikely to happen, but using a fold expression is shorter and clearer. Plus, there is also recursion at compile-time which slows down compilation and may even crash the compiler in improperly bounded cases. This shall never happen with a fold expression
  2. This is pretty hard to source as a fact since everyone prefers to get rid of streams instead of proving they are slow. Since I’m bad at benchmarks (but I’m working on it), I won’t develop that here. If you want to share your own benchmarks (either to prove me wrong or right), please do in the comments.

One thought on “Retrospective: The simplest error handler”

Leave a Reply