在C++中是否应该链接异常?

13

我刚刚完成了一个C++程序,其中我实现了自己的异常(虽然是从std::exception派生的)。当一个异常引起连锁反应,向上传播错误并引发其他异常时,我采用的做法是在每个模块(即类)中的每个适当步骤上串联错误消息。也就是说,旧的异常本身被丢弃,创建一个新的异常,但是具有更长的错误消息。

这种方法在我的小程序中可能有效,但最终我对我的方法并不满意。首先,行号(虽然目前没有应用)和文件名除了最后一个异常外都未被保留;而实际上,这些信息在第一个异常中最为重要。

我认为可以通过将异常链接在一起来更好地处理这个问题;即在新异常的构造函数中提供旧异常。但是这该如何实现呢?难道异常在从方法中超出作用域时不会消失,从而阻止使用异常指针吗?如果异常可以是任何派生类,那么如何复制和存储异常?

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

你对此有何回应?导致其他异常的异常是否应该被链接在一起以保留一种“异常跟踪”--应该如何实现?--或者是否应该使用单个异常并附加额外的数据--应该如何做到这一点?


@kbrimington:确实,这个问题触及到这个问题的核心;即异常链(或“内部异常”);我的问题只是在此基础上进行扩展,并询问是否应该开始这样的工作,还是坚持只抛出一个异常的方法。 - gablin
忘了感谢你提供的链接,但是评论已经无法编辑了。^^ - gablin
作为对问题“C++中异常链的正确方式是什么”的答案(https://dev59.com/xWYr5IYBdhLWcg3w29hI),使用C++11的`std::nested_exception`类是正确的。显然,C++标准库的编写者认为,异常链可以是一件好事情。 - Raedwald
4个回答

6
自从C++11标准发布以来,对于异常处理方面有了显著的改进。在关于异常的讨论中,我经常会错过下面这种嵌套异常的处理方式:

使用 std::nested_exceptionstd::throw_with_nested

在 StackOverflow 上有两篇文章herehere,介绍了如何在代码中不需要调试器或繁琐的日志记录的情况下获取异常的回溯信息,只需编写一个适当的异常处理程序,它将重新抛出嵌套异常。
由于您可以对任何派生的异常类执行此操作,因此您可以向这种回溯中添加大量信息!您还可以查看我的GitHub上的MWE,其中回溯看起来像这样:
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!

@gablin:我不懂Java,但我认为不是这样的。您希望对象具有指向要保存的数据的指针。swapmove将该指针从捕获的对象分配给堆上的对象,而不复制任何内容(这可能很危险)。然后可以销毁捕获的对象,而不影响该数据。 - Potatoswatter
@Potatoswatter:但是只有在捕获的异常中存储的数据已经存储在堆上时,这才有用,对吗?如果不是,那么当异常被销毁时,该数据将被销毁,如果您没有复制该数据而仅通过引用保存它,则指针将指向无效的内存,这将非常不安全。例如,错误消息必须作为string*而不是string存储在异常中。或者我误解了整件事……? - gablin
@gablin:我的观点是最好在创建时将其放置在堆上。然而,非指针的std::string也能满足这一点,而且string实现了swapmove两个函数。 - Potatoswatter
@Potatoswatter:我有点困惑。您能否给一个简短的例子来解释一下区别? - gablin
@gablin:给你。实际上,它不是很短,而是相当完全的功能。如果不真正运行所有内容,就没有办法演示异常对象创建和销毁的语义。 - Potatoswatter
显示剩余3条评论

1

我听说过boost,但从未使用过。我会检查您发布的链接,看看是否包含我正在寻找的答案。谢谢。 - gablin
@gablin - 它基本上是一种良好结构化的允许数据被添加到catch块中异常的方式。这意味着您抛出的所有异常都必须派生自::boost::exception,但如果是这样,将信息添加到异常并使用throw;重新抛出以在链上传播时变得相对轻松。 - Omnifarious
Gablin,对于今天的任何C++编程来说,Boost都是必不可少的。它已经非常成功,以至于其中一些正在成为新标准库的一部分(请参见TR1等)。 - Pavel Radzivilovsky
哦,我知道boost很常见,但没想到这么常见。不过话说回来,我也不能说我一直在用C++。我会确保调查::boost的。再次感谢。 - gablin

0
另一个想法是将相关数据添加到异常对象中,然后使用裸的throw;语句重新抛出它。我认为在这种情况下,堆栈信息仍然会被保留,因此您仍然会知道异常的原始来源,但是测试是一个好主意。
我敢打赌,由于任何堆栈信息是否可用都是实现定义的,因此在裸的throw;语句之后是否以任何方式保留它会更加广泛地有所不同。

如果适用的话,这很优雅,但异常的类型不能改变。 - Potatoswatter
正如Potatoswatter已经提到的,异常类型无法更改,我发现这很麻烦,因为随着堆栈的进一步上升,错误的解释变得更加困难。例如,在顶部方法中捕获IndexOutOfBoundsException并调用用户启动的操作如果在一个访问内部向量的方法中抛出了错误索引,那么这就没有多大意义了。我在这里唯一可能的方法是要么链接它,要么完全放弃它以替换异常。 - gablin
@gablin - 是的,这很有道理,我同意。不幸的是,如果你抛出一个新的异常,我认为没有好的方法来保留关于原始异常的堆栈信息。所以你需要做出选择。我认为::boost::exception是在你的异常中记录信息的好方法,即使你抛出了一个新的异常。它将允许你记录原始异常,我认为这通常是一个好主意。 - Omnifarious

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接