C++中异常的调用栈
Posted
技术标签:
【中文标题】C++中异常的调用栈【英文标题】:Call-stack for exceptions in C++ 【发布时间】:2011-03-14 11:12:27 【问题描述】:今天,在我的 C++ 多平台代码中,我对每个函数都有一个 try-catch。在每个 catch 块中,我将当前函数的名称添加到异常并再次抛出它,以便在最上面的 catch 块(我最终打印异常的详细信息)中,我拥有完整的调用堆栈,这有助于我跟踪异常的原因。
这是一种好的做法,还是有更好的方法来获取异常的调用堆栈?
【问题讨论】:
每个功能?听起来维护起来很有趣。try...catch
围绕 every 函数?这不利于可读性。
预处理器宏可能有助于减少代码重复。
***.com/questions/77005/…的可能重复
它是跨平台的。我使用 Visual Studio 和 gcc。
【参考方案1】:
虽然在此处的答案中提出了相当多的反驳,但我想指出,自从提出这个问题以来,使用 C++11,已经添加了一些方法,允许您以跨平台方式获得良好的回溯,无需调试器或繁琐的日志记录:
使用std::nested_exception
和std::throw_with_nested
在 *** here 和 here 上进行了描述,您可以通过简单地编写一个将重新抛出嵌套异常的适当异常处理程序来在您的代码中获取异常的回溯。
但是,它要求您在要跟踪的函数处插入 try/catch
语句。
由于您可以对任何派生的异常类执行此操作,因此您可以向此类回溯添加大量信息! 你也可以看看我的MWE on GitHub 或"trace" library,其中的回溯看起来像这样:
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】:与 libcsdbg 库链接(请参阅 https://***.com/a/18959030/364818 获取原始答案)看起来是无需修改源代码或第 3 方源代码(即 STL)即可获取堆栈跟踪的最简洁方式。
这使用编译器来检测实际的堆栈集合,这是您真正想要做的。
我没用过它,它受 GPL 污染,但它看起来是正确的想法。
【讨论】:
【参考方案3】:另外一个支持堆栈跟踪的项目:ex_diag。没有宏,存在跨平台,不需要大量代码,工具快速、清晰且易于使用。
这里只需要包裹需要跟踪的对象,如果发生异常就会被跟踪。
【讨论】:
【参考方案4】:有一个不错的小项目提供了漂亮的堆栈跟踪:
https://github.com/bombela/backward-cpp
【讨论】:
【参考方案5】:一个可能更优雅的解决方案是构建一个 Tracer 宏/类。因此,在每个函数的顶部,您可以编写如下内容:
TRACE()
宏看起来像:
Tracer t(__FUNCTION__);
类 Tracer 在构造时将函数名称添加到全局堆栈中,并在销毁时将其自身移除。然后该堆栈始终可用于日志记录或调试,维护要简单得多(一行),并且不会产生异常开销。
实现示例包括 http://www.drdobbs.com/184405270、http://www.codeproject.com/KB/cpp/cmtrace.aspx 和 http://www.codeguru.com/cpp/v-s/debug/tracing/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 可能也值得一看。
无论如何,异常处理方法是粗糙、不灵活且计算量大的。类构造/宏解决方案要快得多,如果需要,可以编译出来用于发布版本。
【讨论】:
如果您使用它来构建您的异常并且可能仅在调试模式下使用。当然能加上参数信息就好了。 值得注意的是,即使在为所有构造对象调用异常析构函数之后。这意味着除非您在具有异常的函数中打印全局堆栈,否则该模型将像正常调用堆栈一样解开。话虽如此,我仍然决定使用它,但我不会从堆栈中删除东西以避免解开。我只知道堆栈中的最后一件事是发生错误的位置(或最接近它的跟踪)。我还添加了一个深度计数器,它在构造时递增,在破坏时递减,就像示例一样。总之是个好主意。 @Dan 是的,它会要求您在抛出并将其放入消息时使用跟踪。然后,普通捕手将能够查看到该点为止生成了哪些跟踪。 至于“全局堆栈”,如果您的代码是多线程的,它当然需要是基于线程的堆栈。【参考方案6】:看看这个SO Question。这可能与您正在寻找的内容接近。它不是跨平台的,但答案为 gcc 和 Visual Studio 提供了解决方案。
【讨论】:
【参考方案7】:不,这太可怕了,我不明白为什么您需要在异常本身中使用调用堆栈 - 我发现异常原因、初始异常发生的代码的行号和文件名已经足够了。
话虽如此,如果你真的必须有一个堆栈跟踪,要做的就是在异常抛出站点生成一次调用堆栈信息。没有单一的可移植方式可以做到这一点,但使用类似 http://stacktrace.sourceforge.net/ 的东西以及类似的 VC++ 库应该不会太难。
【讨论】:
我说的是一个大型项目。这种机制帮助我多次看到导致异常的完整流程。如果它为我节省了大量的调试时间,为什么会如此可怕? @Igor 我们其他人当然只从事小型项目?它可能会节省您的调试时间(尽管如果您花费大量时间进行调试,您还会遇到其他问题),但它会大大降低代码的可维护性和可读性(至少对我而言)更为重要。 @Neil:从另一个角度来看,我发现堆栈跟踪是必不可少的。 @Igor:要在 gcc 中生成堆栈跟踪,***.com/questions/77005/…,我确信 windows 也有类似的东西。无论如何,将这些块包裹在每个函数周围确实是“非常可怕”。 是的,堆栈跟踪很好,没有自己构建它是不可维护的。至少在 Windows 上,当您捕获异常时,您调用GetExceptionInformation
,它会为您提供一个 CONTEXT
结构以传递给 StackWalk64
。
@Ben 你付出的最大代价是便携性,在这种情况下这是一个要求。【参考方案8】:
未处理的异常留给调用函数处理。这种情况一直持续到处理异常为止。无论是否使用函数调用的 try/catch,都会发生这种情况。换句话说,如果调用不在 try 块中的函数,则该函数中发生的异常将自动传递到调用堆栈。因此,您需要做的就是将最顶层的函数放在 try 块中,并在 catch 块中处理异常“...”。该异常将捕获所有异常。所以,你最顶层的函数看起来像
int main()
try
top_most_func()
catch(...)
// handle all exceptions here
如果您想为某些异常设置特定的代码块,您也可以这样做。只需确保这些发生在“...”异常捕获块之前。
【讨论】:
这并不能解决如何生成堆栈跟踪以帮助解决为什么引发异常的问题。【参考方案9】:你所有问题的答案都是一个好的调试器,通常是http://www.gnu.org/software/gdb/在Linux上或Visual Studio在Windows上。它们可以在程序中的任何点按需为您提供堆栈跟踪。
您当前的方法是一个真正令人头疼的性能和维护问题。调试器的发明是为了实现您的目标,但没有开销。
【讨论】:
调试器非常擅长解决可重现的问题。间歇性错误(尤其是那些发生在现场的错误)是堆栈跟踪有益的错误。 崩溃转储 + 调试器 > 堆栈跟踪【参考方案10】:你正在做的不是好习惯。原因如下:
1.没必要。 如果您在调试模式下编译项目以便生成调试信息,您可以轻松地在 GDB 等调试器中获取异常处理的回溯。
2.很麻烦。 这是您必须记住添加到每个功能的内容。如果你碰巧错过了一个函数,那可能会造成很大的混乱,特别是如果那个函数是导致异常的函数。任何查看您的代码的人都必须意识到您在做什么。另外,我打赌你使用了类似 __FUNC__ 或 __FUNCTION__ 或 __PRETTY_FUNCTION__ 之类的东西,遗憾的是它们都是非标准的(C++ 中没有标准的方法来获取函数的名称)。
3.速度很慢。 C++ 中的异常传播已经相当缓慢,添加此逻辑只会使代码路径变慢。如果您使用宏来捕获和重新抛出,这不是问题,您可以轻松地在代码的发布版本中忽略捕获和重新抛出。否则,性能可能会成为问题。
良好做法 虽然在每个函数中捕获并重新抛出以建立堆栈跟踪可能不是一个好习惯,但最好附上最初抛出异常的文件名、行号和函数名。如果您将 boost::exception 与 BOOST_THROW_EXCEPTION 一起使用,您将免费获得此行为。将解释性信息附加到您的异常中也很好,这将有助于调试和处理异常。也就是说,所有这些都应该在构造异常时发生;一旦它被构建,它应该被允许传播到它的处理程序......你不应该反复捕获和重新抛出超过严格必要的。如果您需要在特定函数中捕获并重新抛出以附加一些关键信息,那很好,但是在每个函数中捕获所有异常并为了附加已有信息的目的实在是太多了。
【讨论】:
添加到“它很慢”的情况下,它还阻止了编译器对尾部位置调用的优化。 实际上,如果没有异常发生,开销通常很小,如果确实发生(应该很少见)通常不是很重要。 @Neil,我指的是异常传播的情况。 我相信有零成本的尝试实现。 @Michael Aaron Safyan:需要销毁的对象天气异常是否使用。所以这是一个零和方程。以上是关于C++中异常的调用栈的主要内容,如果未能解决你的问题,请参考以下文章