从异常中恢复上下文

时间:2019-02-02 06:52:03

标签: c++ exception

考虑以下资源管理类

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

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

  1. 将一些可选的上下文信息作为附加参数传递给Ctor,然后可以将其存储在异常中
  2. 在呼叫站点使用pushContext函数。此功能将使用线程本地存储来存储上下文。
  3. 赶上并重新投掷。我认为这种情况下的代码很丑。

1 个答案:

答案 0 :(得分:2)

尽管此解决方案与您的第三个要求 3:不捕获并重新投放相冲突,但我还是提出了一个std::nested_exception和宏的解决方案,因为这似乎为当前的问题提供了合理的解决方案至少对我来说。 我希望这个答案过长可以为您提供帮助。


1。 std::nested_exception

的错误处理

首先,我们可以使用std::nested_exception递归嵌套异常。 粗略地说,我们可以通过调用std::throw_with_nested向该类添加任意类型的异常。 这些使我们能够使用相当简单的代码来携带所有引发异常的信息,只需在上层 中的每个省略号捕获处理程序std::throw_with_nested中将catch(…){ }引发每个异常即可。

例如,以下函数h抛出一个std::nested_exception,该聚集了用户定义的异常SomeExceptionstd::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具有添加以下位置信息的关键作用:

// "..." 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将对我们有用:

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

最后,前三个功能fgh如下重写和简化:

[[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来避免出现this帖子中指出的异常:

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

顺便说一句,最外层的显式try-catch块相当冗长,因此我通常使用this帖子中建议的以下宏:

#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_NESTEDHOOKCATCH_BEGINCATCH_END,我们可以在每个线程中定位异常:

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

HOOK(h());
...

CATCH_END(std::cout)

然后,我们得到以下输出,其中文件名和行号仅是示例。 记录所有可用信息:

DEMO with 2 threads

  

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

     

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

     

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

     

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


2。如果是FooResouce


第一个要求是

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

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

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添加新参数FooResouce::FooResouce并将throw替换为THROW_WITH_NESTED,我们可以在上述错误处理框架内通过第一个要求:

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

下一步,

  

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

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

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

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

CATCH_END(std::cout)

最后,

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

尽管我不知道此要求的详细信息,但是由于我们可以按如下方式在SomeException::getContext中调用output_exceptions_impl并从每次抛出的SomethingExceptions中获取所有上下文,所以我认为我们可以也像这样存储它们:

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