从异常中恢复上下文 [英] Recover context from exception

查看:87
本文介绍了从异常中恢复上下文的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

考虑以下资源管理类

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;
    };

现在,当我们决定捕获异常并格式化错误消息时,很容易知道什么原因导致了基本级别的异常,但是我们没有关于何处的异常在上级中触发的信息。这里的上层是指试图创建FooResource的函数,或者是该堆栈框架上方的任何函数。在需要的情况下,如何在错误中添加上下文:

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. 将一些可选的上下文信息作为附加参数传递给Ctor,然后可以将其存储在异常中

  2. 在呼叫站点使用 pushContext 函数。此函数将使用线程本地存储来存储上下文。

  3. 捕获并重新抛出。我认为这种情况下的代码很丑。

  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.


推荐答案

尽管此解决方案与您的第三个要求 3:不捕获并重新抛出,我提出了一个使用 std :: nested_exception 和宏的解决方案,因为这似乎为当前的问题至少对我而言。
我希望这个答案太长了。

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.

首先,我们可以使用 std :: nested_exception
粗略地说,我们可以通过调用 std :: throw_with_nested 向此类添加任意类型的异常。
这些使我们能够使用相当简单的代码来携带抛出异常的所有信息,只需在每个省略号捕获处理程序<$ c中将每个异常由 std :: throw_with_nested 抛出即可

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.

例如,在函数<$后面的$ c> catch(…){} 在上级 中。 c $ c> h 抛出一个 std :: nested_exception ,它聚合了用户定义的异常 SomeException std :: runtime_error

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."));
    }    
}






定位异常(基本级别)

替换所有这些 std :: throw_with_nested 下面的函数 throw_with_nested_wrapper 通过宏 THROW_WITH_NESTED ,我们可以记录发生异常的文件名和行号。
众所周知, __ FILE __ __ LINE __ 由C ++标准预先定义。
因此,宏 THROW_WITH_NESTED 具有添加以下位置信息的关键作用:

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)...));
};






定位异常(上级)

如果我们必须在上层 中获取有关异常触发位置的信息,则以下宏 HOOK 重用上面的宏 THROW_WITH_NESTED 将对我们有用:

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);            \
    }                                                           \
}()

最后,前三个函数 f g h 被重写并简化如下:

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());
}






提取错误信息

提取嵌套异常的所有解释性信息是一项简单的任务。
将最外部try-catch块处的捕获异常传递到以下函数 output_exceptions_impl 中,我们可以做到这一点。
每个嵌套异常都可以由 递归地抛出。 std :: nested_exception :: rethrow_nested
由于此成员函数在没有存储的异常的情况下调用 std :: terminate ,因此应应用 dynamic_cast 帖子:

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,显式 try-在最外面的地方捕获块非常冗长,因此我通常使用帖子:

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;
    }
}

然后从抛出所有异常h 可以通过以下代码进行跟踪和打印。
插入宏 THROW_WITH_NESTED HOOK CATCH_BEGIN CATCH_END 在我们代码的右行,我们可以在每个线程中找到异常:

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:

具有2个线程的DEMO


运行时错误:prog.cc,l.119,h(

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

运行时错误:prog.cc,l.113,g(),较高级别。

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

运行时错误:prog.cc,l.109,f(),更高级别。

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

逻辑错误:prog.cc,l.105,SomeException,基本

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






2。如果是 FooResouce






第一个要求是


2. In case of FooResouce


First requirement is



  1. 将一些可选的上下文信息作为附加参数传递给Ctor,然后可以将其存储在异常中


让我们定义以下特殊异常类 SomeException 其中包含一些可选的上下文信息和成员函数 getContext 来获取它:

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;
    }
};

在<$中添加新参数 context c $ c> FooResouce :: FooResouce 并将 throw 替换为 THROW_WITH_NESTED ,我们可以通过上述错误处理框架中的第一个要求:

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,


,但我们没有有关异常在何处触发的信息。这里的上层是试图创建FooResource的函数,

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,

创建每个 FooResource HOOK ,我们可以获取有关ctor在上一级发生故障的位置的信息。
呼叫方如下。
这样,将在每个线程中阐明所有错误信息,包括消息,上下文及其位置。

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. 在呼叫站点使用pushContext函数。此功能将使用线程本地存储来存储上下文。


尽管我不知道此要求的详细信息,但由于我们可以按以下方式在 output_exceptions_impl 中调用 SomeException :: getContext ,并从每个抛出 SomethingExceptions ,我想我们也可以像这样存储它们:

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:

演示(我的提案)

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);
    }
    ...
}

这篇关于从异常中恢复上下文的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆