繁体   English   中英

C++ 中的异常调用堆栈

[英]Call-stack for exceptions in C++

今天,在我的 C++ 多平台代码中,我对每个函数都有一个 try-catch。 在每个 catch 块中,我将当前函数的名称添加到异常中并再次抛出它,因此在最上面的 catch 块中(我最终打印异常的详细信息)我有完整的调用堆栈,这有助于我跟踪异常的原因。

这是一个好的做法,还是有更好的方法来获取异常的调用堆栈?

你在做什么不是好的做法。 原因如下:

1.没必要。
如果您在调试模式下编译您的项目以便生成调试信息,您可以轻松地在调试器(如 GDB)中获取异常处理的回溯。

2. 很麻烦。
这是你必须记住添加到每个函数的东西。 如果你碰巧错过了一个函数,那可能会造成很大的混乱,尤其是当那个函数是导致异常的时候。 任何查看您的代码的人都必须意识到您在做什么。 另外,我敢打赌你使用了 __FUNC__ 或 __FUNCTION__ 或 __PRETTY_FUNCTION__ 之类的东西,遗憾的是它们都是非标准的(C++ 中没有标准的方法来获取函数的名称)。

3.它很慢。
C++ 中的异常传播已经相当缓慢,添加此逻辑只会使代码路径变慢。 如果您使用宏来捕捉和重新抛出,这不是问题,您可以在代码的发布版本中轻松地省略捕捉和重新抛出。 否则,性能可能会成为问题。

好的做法
虽然捕获并重新抛出每个函数以构建堆栈跟踪可能不是一个好习惯,但最好附上最初抛出异常的文件名、行号和函数名。 如果您将 boost::exception 与 BOOST_THROW_EXCEPTION 一起使用,您将免费获得此行为。 将有助于调试和处理异常的解释性信息附加到您的异常也很好。 也就是说,所有这些都应该在构造异常时发生; 一旦它被构建,它应该被允许传播到它的处理程序......你不应该重复捕获和重新抛出超过严格必要的次数。 如果您需要在特定函数中捕获并重新抛出以附加一些关键信息,那很好,但是捕获每个函数中的所有异常并为了附加已经可用的信息就太多了。

不,这太可怕了,我不明白为什么你需要在异常本身中调用堆栈——我发现异常原因、最初发生异常的代码的行号和文件名就足够了。

话虽如此,如果您真的必须有一个堆栈跟踪,那么要做的就是在异常抛出站点生成一次调用堆栈信息。 没有单一的可移植方法可以做到这一点,但是使用类似http://stacktrace.sourceforge.net/的东西以及类似的 VC++ 库应该不会太困难。

一种可能更优雅的解决方案是构建 Tracer 宏/类。 所以在每个函数的顶部,你写这样的东西:

TRACE()

宏看起来像:

Tracer t(__FUNCTION__);

类 Tracer 在构建时将函数名称添加到全局堆栈,并在销毁时删除自身。 然后该堆栈始终可用于日志记录或调试,维护简单得多(一行),并且不会产生异常开销。

实施示例包括http://www.drdobbs.com/184405270、http://www.codeproject.com/KB/cpp/cmtrace.aspxhttp://www.codeguru.com/cpp/vs/调试/跟踪/article.php/c4429 此外,像http://www.linuxjournal.com/article/6391这样的 Linux 函数可以更自然地完成它,正如这个 Stack Overflow 问题所描述的: How to generate a stacktrace when my gcc C++ app crashes ACE 的 ACE_Stack_Trace 也值得一看。

无论如何,异常处理方法是粗糙的、不灵活的并且计算量大。 类构造/宏解决方案要快得多,并且可以根据需要编译出来用于发布版本。

有一个不错的小项目可以提供漂亮的堆栈跟踪:

https://github.com/bombela/backward-cpp

您所有问题的答案是一个好的调试器,在 Linux 上通常是http://www.gnu.org/software/gdb/或在 Windows 上是 Visual Studio。 他们可以在程序的任何一点按需为您提供堆栈跟踪。

您当前的方法是一个真正的性能和维护问题。 发明调试器是为了实现您的目标,但没有开销。

看看这个SO 问题 这可能接近您要查找的内容。 它不是跨平台的,但答案为 gcc 和 Visual Studio 提供了解决方案。

另一个支持堆栈跟踪的项目: ex_diag 没有宏,存在跨平台,不需要大量代码,工具快速、清晰且易于使用。

这里只需要包装需要跟踪的对象,如果出现异常就会跟踪到。

与 libcsdbg 库链接(请参阅https://stackoverflow.com/a/18959030/364818获取原始答案)看起来是无需修改源代码或第 3 方源代码(即 STL)即可获取堆栈跟踪的最简洁方法。

这使用编译器来检测实际的堆栈集合,这是您真正想要做的。

我没用过它,它被 GPL 污染了,但它看起来是个正确的主意。

虽然这里的答案中提出了很多反驳论点,但我想指出的是,自从提出这个问题以来, C++11已经添加了一些方法,这些方法允许您以跨平台的方式获得很好的回溯,并且无需调试器或繁琐的日志记录:

使用std::nested_exceptionstd::throw_with_nested

它在 StackOverflow herehere上进行了描述,您如何通过简单地编写一个将重新抛出嵌套异常的适当异常处理程序来获取代码中异常的回溯 但是,它会要求您在要跟踪的函数中插入try/catch语句。

由于您可以对任何派生的异常类执行此操作,因此您可以向此类回溯添加大量信息! 您还可以查看我在 GitHub 上的 MWE或我的“trace”库,其中回溯看起来像这样:

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"

未处理的异常留给调用函数处理。 这一直持续到处理异常为止。 无论有没有 try/catch 函数调用都会发生这种情况。 换句话说,如果调用的函数不在 try 块中,则该函数中发生的异常将自动向上传递到调用堆栈。 因此,您需要做的就是将最顶层的函数放在 try 块中,并在 catch 块中处理异常“...”。 该异常将捕获所有异常。 所以,你最顶层的功能看起来像

int main()
{
  try
  {
    top_most_func()
  }
  catch(...)
  {
    // handle all exceptions here
  }
}

如果你想为某些异常使用特定的代码块,你也可以这样做。 只要确保那些发生在“...”异常捕获块之前。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM