C++ 从异常恢复上下文

C++ 从异常恢复上下文,c++,exception,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_f

考虑以下资源管理类

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的函数,或该堆栈框架之上的任何函数。如果需要,您将如何向错误添加上下文:

  • 将一些可选的上下文信息作为额外参数传递给Ctor,然后将其存储在异常中
  • 在调用站点使用
    pushContext
    函数。此函数将使用线程本地存储来存储上下文
  • 捕捉并重新捕获。我认为在这种情况下,代码会很难看

  • 尽管此解决方案与您的第三个要求3:无捕获和重新捕获冲突,但我建议使用
    std::nested_exception
    和宏的解决方案,因为至少对我来说,这似乎为当前问题提供了合理的解决方案。 我希望这个太长的答案能帮助你


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

    例如,下面的函数
    h
    抛出一个
    std::nested_异常
    ,它聚合了用户定义的异常
    SomeException
    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."));
        }    
    }
    

    查找异常(基本级别)

    通过宏
    throw\u with_nested
    ,我们可以记录发生异常的文件名和行号,通过以下函数
    throw\u with_nested\u wrapper
    替换所有这些
    std::throw\u with_nested
    。 众所周知,代码C++ >代码> >代码> > 因此,宏
    THROW\u 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)...));
    };
    
    最后,前三个函数
    f
    g
    h
    被重写和简化如下:

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

    提取错误信息

    提取嵌套异常的所有解释性信息是一项简单的任务。 将最外层的try-catch块捕获的异常传递到下面的函数
    output\u exceptions\u impl
    ,我们就可以做到这一点。 每个嵌套异常都可以由递归抛出。 由于此成员函数在没有存储的异常时调用
    std::terminate
    ,因此我们应该应用
    dynamic\u cast
    来避免它,正如本文中所指出的:

    然后,从
    h
    抛出的所有异常都可以通过以下代码进行跟踪和打印。 在代码的右行插入嵌套的宏
    THROW\u
    HOOK
    CATCH\u BEGIN
    CATCH\u END
    ,我们可以找到每个线程中的异常:

    CATCH_BEGIN // most outer try-catch block in each thread
    ...
    
    HOOK(h());
    ...
    
    CATCH_END(std::cout)
    
    然后我们得到以下输出,其中文件名和行号只是一个示例。 所有可用信息都会记录下来:

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

    运行时错误:prog.cc,l.113,g(),上层

    运行时错误:prog.cc,l.109,f(),上层

    逻辑错误:prog.cc,l.105,某些异常,基本级别


    2.如果是
    fooresource

    第一个要求是

  • 将一些可选的上下文信息作为额外参数传递给Ctor,然后将其存储在异常中
  • 让我们定义以下特殊异常类
    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
    添加到
    fooResourcece::fooResourcece
    并将
    throw
    替换为
    throw\u和_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的函数

    使用
    HOOK
    创建每个
    FooResource
    ,我们可以获得关于在上层中ctor失败的位置的信息。 调用方将如下所示。 通过这种方式,所有错误信息(包括消息、上下文及其位置)都将在每个线程中得到澄清

    CATCH_BEGIN // most outer try-catch block in each thread
    ...
    
    auto resource = HOOK(FooResouce(T(), "context"));
    ...
    
    CATCH_END(std::cout)
    

    最后,

  • 在callsite中使用pushContext函数。此函数将使用线程本地存储来存储上下文
  • 虽然我不知道这个需求的细节,但是由于我们可以在
    output\u exceptions\u impl
    中调用
    SomeException::getContext
    ,如下所示,并从每个抛出的
    SomethingExceptions
    中获取所有上下文,我认为我们也可以这样存储它们:

    void output\u exceptions\u impl(
    常量标准::异常和异常,
    标准::ostream&stream,
    bool isFirstCall=false)
    {
    ...
    catch(const SomeException&e){//添加此部分。
    流动
    
    可能上下文的
    \uuuu文件和
    \uuuuu行和
    \uuuuuu行宏?@TrebuchetMS文件和行当前不在我感兴趣的上下文中,可能是
    std::nested\u exception
    ?顺便说一句,
    \uuuuu行和调试器有什么问题?@Hiroki std::nested\u exception与(3)相同,对吗?调试器?不能创建资源不是一个bug。@user877329仅仅因为它不是bug并不意味着禁止您启动调试器并使用它跟踪
    
    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;
        }
    };
    
    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);
            }
            ...
        }
        ...
    };
    
    CATCH_BEGIN // most outer try-catch block in each thread
    ...
    
    auto resource = HOOK(FooResouce(T(), "context"));
    ...
    
    CATCH_END(std::cout)
    
    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);
        }
        ...
    }