【问题标题】:Should exceptions be chained in C++? [duplicate]异常应该在 C++ 中链接吗? [复制]
【发布时间】:2010-08-22 22:31:20
【问题描述】:

我刚刚完成了一个 C++ 程序的工作,在该程序中我实现了自己的异常(尽管派生自 std::exception)。当一个异常导致连锁反应、向上传播错误并引发其他异常时,我采用的做法是在模块(读取类)的每个适当步骤中连接错误消息。 IE。旧异常本身被删除并创建一个新异常,但错误消息更长。

这可能对我的小程序有用,但我最终对我的方法不太满意。一方面,除了最后一个例外,不保留行号(虽然目前没有应用)和文件名;确实,第一个例外中最感兴趣的信息。

我认为这可以通过将异常链接在一起来更好地处理;即旧异常在新异常的构造函数中提供。但这将如何实施?当它们超出方法的范围时,异常是否会消失,从而阻止使用异常指针?如果异常可以是任何派生类,如何复制和存储异常?

这最终让我考虑在 C++ 中链接异常是否是一个好主意。也许应该只创建一个异常,然后向其中添加其他数据(就像我一直在做的那样,但可能以更好的方式)?

您对此有何回应?是否应该将由另一个引起的异常链接在一起以保留一种“异常跟踪”——应该如何实现? -- 还是应该使用单个异常并附加额外的数据 -- 应该如何做?

【问题讨论】:

  • @kbrimington:确实,这个问题确实触及了这个问题的核心。即异常链接(或“内部异常”);我的问题只是对此进行了扩展,还询问是否应该从一开始就进行此类承诺,还是坚持只扔一次的方法。
  • 忘记感谢您提供链接,但无法再编辑评论。 ^^
  • what is the proper way to do exception chaining in C++ 问题的答案是使用 C++11 std::nested_exception 类,显然 C++ 标准库的作者认为,是的,链接异常可能是一件好事去做。

标签: c++ exception chaining nested-exceptions


【解决方案1】:

自从提出这个问题以来,C++11 的标准已经发生了显着的变化。 在有关异常的讨论中,我一直在忽略这一点,但以下方法(嵌套异常)可以解决问题:

使用std::nested_exceptionstd::throw_with_nested

它在 StackOverflow herehere 上进行了描述,您可以在代码中获取异常的回溯,而无需调试器或繁琐的日志记录,只需编写适当的异常将重新抛出嵌套异常的处理程序。

由于您可以对任何派生的异常类执行此操作,因此您可以向此类回溯添加大量信息! 你也可以看看我的MWE on GitHub,回溯看起来像这样:

Library API: Exception caught in function 'api_function'
Backtrace:
~/Git/mwe-cpp-exception/src/detail/Library.cpp:17 : library_function failed
~/Git/mwe-cpp-exception/src/detail/Library.cpp:13 : could not open file "nonexistent.txt"

【讨论】:

    【解决方案2】:

    如果您希望它比接收它的catch 块寿命长,除了throw; 重新抛出之外,有必要将数据从异常对象中复制到一个链中。 (其中包括,例如,如果 catch 块通过 throw obj; 退出。)

    这可以通过将要保存的数据放在堆上,并在异常内的私有数据上实现swap(C++0x 中的move)来完成,例如。

    当然,在使用带有异常的堆时需要小心……但话又说回来,在大多数现代操作系统中,内存过度使用完全阻止了 new 抛出,无论好坏。良好的内存余量和在完全崩溃时从链中删除异常应该保证它的安全。

    struct exception_data { // abstract base class; may contain anything
        virtual ~exception_data() {}
    };
    
    struct chained_exception : std::exception {
        chained_exception( std::string const &s, exception_data *d = NULL )
            : data(d), descr(s) {
            try {
                link = new chained_exception;
                throw;
            } catch ( chained_exception &prev ) {
                swap( *link, prev );
            } // catch std::bad_alloc somehow...
        }
    
        friend void swap( chained_exception &lhs, chained_exception &rhs ) {
            std::swap( lhs.link, rhs.link );
            std::swap( lhs.data, rhs.data );
            swap( lhs.descr, rhs.descr );
        }
    
        virtual char const *what() const throw() { return descr.c_str(); }
    
        virtual ~chained_exception() throw() {
            if ( link && link->link ) delete link; // do not delete terminator
            delete data;
        }
    
        chained_exception *link; // always on heap
        exception_data *data; // always on heap
        std::string descr; // keeps data on heap
    
    private:
        chained_exception() : link(), data() {}
        friend int main();
    };
    
    void f() {
        try {
            throw chained_exception( "humbug!" );
        } catch ( std::exception & ) {
            try {
                throw chained_exception( "bah" );
            } catch ( chained_exception &e ) {
                chained_exception *ep = &e;
                for ( chained_exception *ep = &e; ep->link; ep = ep->link ) {
                    std::cerr << ep->what() << std::endl;
                }
            }
        }
    
        try {
            throw chained_exception( "meh!" );
        } catch ( chained_exception &e ) {
            for ( chained_exception *ep = &e; ep->link; ep = ep->link ) {
                std::cerr << ep->what() << std::endl;
            }
        }
    }
    
    int main() try {
        throw chained_exception(); // create dummy end-of-chain
    } catch( chained_exception & ) {
        // body of main goes here
        f();
    }
    

    输出(适当地脾气暴躁):

    bah
    humbug!
    meh!
    

    【讨论】:

    • 我认为“交换”或“移动”相当于 Java 的“克隆”,是吗?
    • @gablin:我不知道 Java,但我不这么认为。您希望对象具有指向要保存的数据的指针。 swapmove 将该指针从捕获的对象分配给堆上的对象,而不复制任何内容(这可能是有风险的)。然后可以销毁捕获的对象而不影响该数据。
    • @Potatoswatter:但这只有在捕获的异常中存储的数据已经存储在堆上时才有用,对吗?如果不是,那么当异常被销毁时,该数据将被销毁,并且如果您没有复制该数据而只是通过引用保存它,那么您将遇到非常不安全的情况,指针指向无效内存。例如,错误消息必须作为字符串*而不是字符串存储在异常中。还是我误解了整个事情......?
    • @gablin:我的观点是最好在创建时将它放在堆上。但是,非指针 std::string 确实满足这一点,而且 string 实现了 swapmove
    • @Potatoswatter:我很困惑。您能否举一个简短的例子来解释其中的区别?
    【解决方案3】:

    你可能想看看这个:http://www.boost.org/doc/libs/1_43_0/libs/exception/doc/boost-exception.html

    这与 MS 在 C# 中所做的方法有些不同,但它似乎符合您的要求。

    【讨论】:

    • 我听说过 boost,但一直没有使用它。我将检查您发布的链接,看看它是否包含我正在寻找的答案。谢谢。
    • @gablin - 它基本上是一种结构良好的方法,允许将数据添加到catch 块中的异常中。这意味着您抛出的所有异常都必须从 ::boost::exception 派生,但如果是,则向异常添加信息并在其向上传播时使用 throw; 重新抛出相对容易。
    • gablin,boost 是当今任何 C++ 编程的必备工具。它非常成功,其中一些正在成为新标准库的一部分(参见 TR1 等。)
    • 哦。好吧,我知道提升很常见,但并不那么常见。再说一次,我也不能说我一直在使用 C++。我将确保调查 [code]::boost[/code]。再次感谢。
    【解决方案4】:

    另一个想法是将相关数据添加到您的异常对象中,然后使用裸throw; 语句重新抛出它。我认为在这种情况下会保留堆栈信息,因此您仍然会知道异常的原始来源,但测试是个好主意。

    我敢打赌,因为任何堆栈信息是否可用都是由实现定义的,所以在裸露的throw; 语句之后,无论它是否以任何方式保留,实现都会有更大的差异。

    【讨论】:

    • 这很优雅,如果它适用,但异常的类型不能改变。
    • 正如 Potatoswatter 已经提到的,异常的类型不能改变,我发现这很麻烦,因为错误的解释越往上越困难。例如,在调用用户启动操作的顶级方法中捕获 IndexOutOfBoundsException 没有多大意义,如果它在发生访问具有错误索引的内部向量的方法中被丢弃。我在这里看到的唯一可能的方法是链接它或完全放弃它以替换异常。
    • @gablin - 是的,这是有道理的,我同意。不幸的是,如果您抛出一个新异常,我认为没有任何好的方法可以保留有关原始异常的堆栈信息。所以你有一个选择。我认为::boost::exception 是在异常中记录信息的好方法,即使你抛出一个新异常也是如此。它可以让您录制原件,我认为这通常是个好主意。
    猜你喜欢
    • 2014-10-18
    • 2010-09-10
    • 2013-04-13
    • 2011-02-11
    • 2013-05-20
    • 1970-01-01
    • 1970-01-01
    • 2013-05-01
    • 1970-01-01
    相关资源
    最近更新 更多