首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >从异常恢复上下文

从异常恢复上下文
EN

Stack Overflow用户
提问于 2019-02-02 06:52:02
回答 1查看 237关注 0票数 3

考虑以下资源管理类

代码语言:javascript
复制
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的函数,或者该堆栈框架之上的任何函数。如果需要的话,如何在错误中添加上下文:

  1. 将一些可选的上下文信息作为额外的参数传递给Ctor,然后将其存储在异常中。
  2. 在调用站点时,使用pushContext函数。此函数将使用线程本地存储存储上下文。
  3. 接住并重投。我觉得在这种情况下代码会很难看。
EN

回答 1

Stack Overflow用户

发布于 2019-02-02 17:29:27

虽然该解决方案与您的第三个需求3:无捕获和重新抛出相冲突,但我建议使用std::nested_exception和宏解决方案,因为这似乎为当前问题提供了一个合理的解决方案,至少对我来说是这样。我希望这个太长的答案能帮到你。

1.使用std::nested_exception进行错误处理

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

例如,下面的函数h抛出一个聚合用户定义的异常SomeExceptionstd::runtime_errorstd::nested_exception

代码语言:javascript
复制
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有一个关键角色来添加以下位置信息:

代码语言:javascript
复制
// "..." 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将适用于我们:

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

最后,将前三个函数fgh重写并简化如下:

代码语言:javascript
复制
[[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 post中所指出的那样:

代码语言:javascript
复制
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 post中提出的以下宏:

代码语言:javascript
复制
#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,我们可以在每个线程中找到异常:

代码语言:javascript
复制
CATCH_BEGIN // most outer try-catch block in each thread
...

HOOK(h());
...

CATCH_END(std::cout)

然后,我们得到以下输出,其中文件名和行号只是一个例子。所有可获得的资料均记录在案:

DEMO with 2 threads

运行时错误: prog.cc,1.119,h(),较高级别。 运行时错误: prog.cc,l.113,g(),较高级别。 运行时错误: prog.cc,l.109,f(),较高级别。 逻辑错误: prog.cc,1.105,SomeException,基本级别。

2.如属FooResouce

第一项要求是

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

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

代码语言:javascript
复制
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,我们可以在上面的错误处理框架中传递第一个需求:

代码语言:javascript
复制
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在上层失败的位置的信息。呼叫方如下所示。这样,所有错误信息(包括消息、上下文及其位置)都将在每个线程中得到澄清。

代码语言:javascript
复制
CATCH_BEGIN // most outer try-catch block in each thread
...

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

CATCH_END(std::cout)

最后,

  1. 在调用站点时,使用pushContext函数。此函数将使用线程本地存储存储上下文。

虽然我不知道这个需求的细节,但是由于我们可以按下面的方式在SomeException::getContext中调用output_exceptions_impl并从每个抛出的SomethingExceptions中获取所有上下文,所以我认为我们也可以这样存储它们:

DEMO(my proposal)

代码语言:javascript
复制
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);
    }
    ...
}
票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/54490757

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档