今天,在我的 C++ 多平台代码中,我对每个函数都进行了尝试。在每个 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.aspx和 http://www.codeguru.com/cpp/vs /debug/tracing/article.php/c4429。像http://www.linuxjournal.com/article/6391这样的Linux 函数也可以更本地地完成它,正如 Stack Overflow 问题所述:How to generate a stacktrace when my gcc C++ app crashs。ACE 的 ACE_Stack_Trace 可能也值得一看。
无论如何,异常处理方法是粗糙的、不灵活的和计算量大的。类构造/宏解决方案要快得多,如果需要,可以编译出来用于发布版本。
你所有问题的答案是一个很好的调试器,通常是http://www.gnu.org/software/gdb/在 linux 或 Visual Studio 在 Windows。它们可以在程序中的任何点按需为您提供堆栈跟踪。
您当前的方法是真正令人头疼的性能和维护问题。发明调试器是为了实现您的目标,但没有开销。
有一个不错的小项目提供了漂亮的堆栈跟踪:
看看这个SO Question。这可能与您正在寻找的内容接近。它不是跨平台的,但答案为 gcc 和 Visual Studio 提供了解决方案。
另一个支持堆栈跟踪的项目:ex_diag。没有宏,存在跨平台,不需要大量代码,工具快速,清晰且易于使用。
这里只需要封装需要跟踪的对象,如果发生异常就会被跟踪。
与 libcsdbg 库链接(有关原始答案,请参见https://stackoverflow.com/a/18959030/364818)看起来是在不修改源代码或第 3 方源代码(即 STL)的情况下获取堆栈跟踪的最简洁方法。
这使用编译器来检测实际的堆栈集合,这是您真正想要做的。
我没有使用它,它被 GPL 污染,但它看起来是正确的想法。
虽然在这里的答案中提出了很多反对论点,但我想指出,自从提出这个问题以来,使用C++11添加了一些方法,这些方法允许您以跨平台的方式获得良好的回溯,并且无需调试器或繁琐的日志记录:
std::nested_exception
和std::throw_with_nested
它在此处和此处的 StackOverflow 上进行了描述,如何通过简单地编写将重新抛出嵌套异常的适当异常处理程序来获取代码中异常的回溯。但是,它将要求您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
}
}
如果您想为某些异常提供特定的代码块,您也可以这样做。只需确保这些发生在“...”异常捕获块之前。