3

Consider the following resource managing class

class FooResouce
    {
    public:
        explicit FooResouce(T arg_to_construct_with)
            {
            m_foo = create_foo_resouce(arg_to_construct_with);
            if(m_foo == nullptr)
                {throw SomeException(get_foo_resource_error(), arg_to_construct_with);}
            }
        // Dtor and move operations
        // Other FooResource interaction methods
    private:
        foo_resource_t* m_foo;
    };

Now, when we decide to catch the exception and format an error message, it's easy to know what caused the exception at the fundamental level, but we have no information about where the exception was triggered in the upper level. Here upper level refers the function that tried to create a FooResource, or any function above that stack frame. How would you add context to the error in case that is needed:

  1. Pass some optional context information as an extra argument to the Ctor, which can then be stored in the exception
  2. At callsite use a pushContext function. This function will store the context using thread-local storage.
  3. Catch and rethrow. I think the code would be ugly in this case.
Bhargav Rao
  • 50,140
  • 28
  • 121
  • 140
user877329
  • 6,717
  • 8
  • 46
  • 88
  • Maybe the `__FILE__` and `__LINE__` macros for context? – TrebledJ Feb 02 '19 at 06:59
  • @TrebuchetMS __FILE__ and __LINE__ is currently not in the context I am interested in – user877329 Feb 02 '19 at 07:49
  • Maybe `std::nested_exception`? BTW, what's wrong with `__FILE__`, `__LINE__` and debuggers? – Hiroki Feb 02 '19 at 08:00
  • @Hiroki std::nested_exception is same as (3), right? Debuggers? It is not a bug that the resource could not be created. – user877329 Feb 02 '19 at 08:05
  • @user877329 thx. Ah I see. Your third requirement is 3: **No** catch and rethrow. So do you want to use exceptions for normal (recoverable) code flow? Exceptions should be reserved for situations which are truly exceptional. – Hiroki Feb 02 '19 at 08:43
  • @Hiroki Recoverable *where*? And there is no other way to fail a ctor, and I do not like to put the resource into an invalid state, which might be a different approach. – user877329 Feb 02 '19 at 09:02
  • @user877329 *no other way to fail a ctor* : completely agree with you and that is a true exceptional case. – Hiroki Feb 02 '19 at 09:05
  • 1
    @user877329 Just because it's not a bug doesn't mean you are banned from spinning up your debugger and using it to trace the execution flow of your program. – Lightness Races in Orbit Feb 03 '19 at 12:40
  • You might want to look at Boost.Exception, which supports adding arbitrary data to exception objects and provides convenient ways to add common things like file and line, error codes, and error message strings. – aschepler Feb 09 '19 at 15:34

1 Answers1

2

Although this solution conflicts with your third requirement 3: No Catch and rethrow, I propose an solution with std::nested_exception and macros because this seems to provide a reasonable solution for the current problem at least to me. I hope this too long answer can help you.


1. An Error Handling with std::nested_exception


First, we can recursively nest exceptions using std::nested_exception. Roughly speaking, We can add exceptions of arbitrary types to this class by calling std::throw_with_nested. These enables us to carry all information of thrown exceptions with a rather simple code, just throwing each exception by std::throw_with_nested in each ellipsis catch handler catch(…){ } in the upper level.

For instance, following function h throws a std::nested_exception which aggregates the user-defined exception SomeException and std::runtime_error:

struct SomeException : public std::logic_error {
    SomeException(const std::string& message) : std::logic_error(message) {}
};

[[noreturn]] void f(){
    std::throw_with_nested(SomeException("error."));
}

[[noreturn]] void g()
{
    try {
        f();
    }
    catch (...) {
        std::throw_with_nested(std::runtime_error("Error of f."));
    }
};

[[noreturn]] void h()
{
    try {
        g();
    }
    catch (...) {
        std::throw_with_nested(std::runtime_error("Error of g."));
    }    
}

Locating Exceptions (Fundamental Level)

Replacing all of these std::throw_with_nested by the following function throw_with_nested_wrapper through the macro THROW_WITH_NESTED, we can make records of the file names and the line numbers where exceptions occurred. As is well known, __FILE__ and __LINE__ are pre-defined by C++ standard. So the macro THROW_WITH_NESTED has a key role to add these location information:

// "..." are arguments of the ctor of ETYPE
// and the first one must be a string literal.
#define THROW_WITH_NESTED(ETYPE, ...)  \
throw_with_nested_wrapper<ETYPE>(__FILE__, __LINE__, __VA_ARGS__);

template<typename E, typename ...Args>
[[noreturn]]
void throw_with_nested_wrapper(
    char const* fileName,
    std::size_t line,
    const std::string& message,
    Args&& ...args)
{
    auto info = std::string(fileName)
                         + ", l." + std::to_string(line) + ", " 
                         + message;
    
    std::throw_with_nested(E(info, std::forward<decltype(args)>(args)...));
};

Locating Exceptions (Upper Level)

If we have to pick up information about where the exception was triggered in the upper level, the following macro HOOK reusing the above macro THROW_WITH_NESTED would work for us:

#define HOOK(OPERATION)                                         \
[&]()                                                           \
{                                                               \
    try{                                                        \
        return OPERATION;                                       \
    }                                                           \
    catch(...){                                                 \
        auto info = std::string(#OPERATION) + ", upper level."; \
        THROW_WITH_NESTED(std::runtime_error, info);            \
    }                                                           \
}()

Finally, first three functions f, g and h are rewritten and simplified as follows:

[[noreturn]] void f(){
    THROW_WITH_NESTED(SomeException, "SomeException, fundamental level.");
}

void g(){
    HOOK(f());
};

void h(){
    HOOK(g());
}

Extracting Error Information

Extracting all explanatory information of nested exceptions is a simple task. Passing the caught exception at the most outer try-catch block into the following function output_exceptions_impl, we can do it. Each nested exception can be recursively thrown by std::nested_exception::rethrow_nested. Since this member function calls std::terminate when there is no stored exception, we should apply dynamic_cast to avoid it as pointed out in this post:

template<typename E>
std::enable_if_t<std::is_polymorphic<E>::value>
rethrow_if_nested_ptr(const E& exception)
{
    const auto *p = 
        dynamic_cast<const std::nested_exception*>(std::addressof(exception));

    if (p && p->nested_ptr()){
        p->rethrow_nested();
    }
}

void output_exceptions_impl(
    const std::exception& exception,
    std::ostream& stream,
    bool isFirstCall = false)
{
    try 
    {
        if (isFirstCall) { throw; }

        stream << exception.what() << std::endl;
        rethrow_if_nested_ptr(exception);
    }
    catch (const std::runtime_error& e) {
        stream << "Runtime error: ";
        output_exceptions_impl(e, stream);
    }
    /* ...add further catch-sections here... */
    catch(...){
        stream << "Unknown Error.";
    }
}

BTW, explicit try-catch blocks at the most outer places are rather verbose and thus I usually use the following macro proposed in this post:

#define CATCH_BEGIN          try{
#define CATCH_END(OSTREAM)   } catch(...) { output_exceptions(OSTREAM); }

void output_exceptions(std::ostream& stream)
{
    try {
        throw;
    }
    catch (const std::exception& e) {
        output_exceptions_impl(e, stream, true);
    }
    catch (...) {
        stream << "Error: Non-STL exceptions." << std::endl;
    }
}

Then all exceptions thrown from h can be traced and printed by the following code. Inserting the macros THROW_WITH_NESTED, HOOK, CATCH_BEGIN and CATCH_END at the right lines of our code, we can locate exceptions in each thread:

CATCH_BEGIN // most outer try-catch block in each thread
...

HOOK(h());
...

CATCH_END(std::cout)

Then we get the following output where file names and line numbers are just an examples. All the available information is recorded:

DEMO with 2 threads

Runtime error: prog.cc, l.119, h(), upper level.

Runtime error: prog.cc, l.113, g(), upper level.

Runtime error: prog.cc, l.109, f(), upper level.

Logic error: prog.cc, l.105, SomeException, fundamental level.


2. In case of FooResouce


First requirement is

  1. Pass some optional context information as an extra argument to the Ctor, which can then be stored in the exception

Let us define the following special exception class SomeException which contains some optional context information and the member function getContext to get it:

class SomeException : public std::runtime_error
{
    std::string mContext;
    
public:
    SomeException(
        const std::string& message,
        const std::string& context)
        : std::runtime_error(message), mContext(context)
    {}
    
    const std::string& getContext() const noexcept{
        return mContext;
    }
};

Adding a new argument context to FooResouce::FooResouce and replacing throw by THROW_WITH_NESTED, we can pass the first requirement within the above error handling framework:

class FooResouce
{
public:
    FooResouce(
        T arg_to_construct_with,
        const std::string& context)
    {
        m_foo = create_foo_resouce(arg_to_construct_with);
        if(!m_foo){
            THROW_WITH_NESTED(SomeException, "Ctor failed.", context);
        }
        ...
    }
    ...
};

Next,

but we have no information about where the exception was triggered in the upper level. Here upper level refers the function that tried to create a FooResource,

Creating each FooResource with HOOK, we can get information about where the ctor was failed in the upper level. The caller side would be as follows. In this manner, all error information including messages, contexts and their locations would be clarified in each thread.

CATCH_BEGIN // most outer try-catch block in each thread
...

auto resource = HOOK(FooResouce(T(), "context"));
...

CATCH_END(std::cout)

Finally,

  1. At callsite use a pushContext function. This function will store the context using thread-local storage.

Although I don't know the detail of this requirement, but since we can call SomeException::getContext in output_exceptions_impl as follows and get all contexts from every thrown SomethingExceptions, I think we can also store them like this:

DEMO(my proposal)

void output_exceptions_impl(
    const std::exception& exception,
    std::ostream& stream,
    bool isFirstCall = false)
{
    ...
    catch (const SomeException& e) { // This section is added.
        stream 
            << "SomeException error: context:" 
            << e.getContext() << ", "; // or pushContext?
        output_exceptions_impl(e, stream);
    }
    ...
}
Community
  • 1
  • 1
Hiroki
  • 2,780
  • 3
  • 12
  • 26