使用catch(...)(省略号)进行事后分析

21

在另一个问题中,有人建议使用catch(...)来捕获所有未处理的异常,即围绕整个main()放置try{}catch(...){}块。

这听起来是一个有趣的想法,可以节省大量调试程序的时间,并留下至少一些发生了什么的提示。

问题的关键是通过这种方式可以恢复哪些信息(除了我留下的任何调试全局变量),以及如何恢复它(如何访问和识别使用了哪个catch)

此外,还有哪些注意点。特别是:

  • 它会与后来出现的线程相容吗?
  • 它不会破坏处理段错误(在其他地方被捕获为信号)的能力吗?
  • 它不会影响到必然嵌套在内部以处理预期异常的其他try...catch块吗?
6个回答

18

是的,这是一个好主意。

如果让异常逃逸main函数,那么在应用程序关闭前是否卸载栈是由实现定义的。因此,在我看来,捕获main函数中的所有异常非常重要。

然后问题就变成了如何处理这些异常。
一些操作系统(例如微软和SE)提供了一些额外的调试工具,因此在捕获异常后只需重新抛出异常即可(因为栈已经被卸载了)。

int main()
{
    try
    {
        /// All real code
    }
    // I see little point in catching other exceptions at this point 
    // (apart from better logging maybe). If the exception could have been caught
    // and fixed you should have done it before here.

    catch(std::exception const& e)
    {
         // Log e.what() Slightly better error message than ...
         throw;
    }
    catch(...)   // Catch all exceptions. Force the stack to unwind correctly.
    {
        // You may want to log something it seems polite.
        throw;  // Re-throw the exception so OS gives you a debug opportunity.
    }
}
  • 它会对随后出现的线程产生影响吗?

它不应该对线程产生影响。通常情况下,您需要手动加入任何子线程以确保它们已经退出。当主线程退出时,子线程的具体情况并没有得到很好的定义(因此请阅读您的文档),但通常所有子线程都会立即死亡(一种不包括解开其堆栈的可怕死亡方式)。

如果您在谈论子线程中的异常。同样,这并没有得到很好的定义(因此请阅读您的文档),但是如果一个线程通过异常退出(即用于启动线程的函数由于异常而不是返回而退出),那么这通常会导致应用程序终止(与上述相同的影响)。因此,最好停止所有异常从一个线程中退出。

  • 它不会破坏处理段错误(在其他地方捕获为信号)吗?

信号不受异常处理机制的影响。
但是,因为信号处理程序可能在堆栈上放置奇怪的结构(用于其自身返回到正常代码的返回处理),所以从信号处理程序中抛出异常不是一个好主意,因为这可能会导致意外结果(并且绝对不可移植)。

  • 它不会影响必然嵌套在内部用于处理预期异常的其他try...catch块吗?

它不应该对其他处理程序产生影响。


7
据我所记,Win32平台上的catch(...)也可以捕获SEH异常,而您不希望这样做。如果发生了SEH异常,那么一定是发生了非常可怕的事情(主要是访问冲突),因此您不能再信任您的环境。几乎所有您可能尝试的操作都可能导致另一个SEH异常,因此根本不值得尝试。此外,某些SEH异常是系统意图捕获的; 更多信息请参见这里

因此,我的建议是为您所有的异常使用基础异常类(例如std::exception),并在“catch-all”中仅捕获该类型; 由于其他类型的异常未知,因此您的代码无法准备处理它们。


如果我以 throw; 结尾我的 catch 块会怎样?无论如何,当SEH发生时,除了进入SEH递归(然后看门狗就会杀死我),不会发生更坏的情况。 - SF.
即使您重新抛出异常,仍然会有一些正常情况被视为异常处理(例如,在堆栈保护页面上发生访问冲突,由系统自动扩展堆栈来处理)。 如果在异常处理程序中生成SEH异常,则不会被catchall捕获(为此,您需要设置全局SEH处理程序),而是应用程序将崩溃;但是,这将使minidump失去意义,因为所有SEH异常都将追溯到catchall而不是真正的有问题的代码。 - Matteo Italia
我会将这个作为可选的调试工具。如果非段错误异常引起问题,就开启它;否则通常关闭它。 - SF.
3
在Windows下,catch(...)是否能捕获SEH异常取决于编译器。对于微软编译器,vc7的catch(...)总是能捕获SEH异常。从vc8开始,有一个编译选项可以启用这种行为(/EHa),但默认情况下未开启。 - JoeG
有趣,我不知道这个(实际上我仍然使用7.1,所以我只知道它的行为)。 - Matteo Italia

4

全局try catch块在生产系统中非常有用,可以避免向用户显示不友好的消息。但在开发过程中,我认为最好避免使用。

关于您的问题:

  • 我认为全局catch块不会捕获另一个线程中的异常。每个线程都有自己的堆栈空间。
  • 我不确定。
  • 嵌套的try...catch块不受影响,将像往常一样执行。异常沿着堆栈向上传播,直到找到一个try块。

4
如果“避免显示不友好的信息”意味着“用可读的信息替换不友好的信息”,那我同意。如果你只是想移除错误信息,那么这只会让用户感到困惑。 - Tor Valamo
1
这就是我的意思,向用户显示可读的消息而不是解密的堆栈跟踪。 - kgiannakakis
在大多数系统中,如果异常逃离了线程入口点,则应用程序会不带任何提示地终止。这会导致应用程序停止而不会解开主线程堆栈。但是请仔细阅读您的线程文档以获取详细信息。但通常最好在线程基础处捕获所有异常。 - Martin York

2
如果您正在制作一个.NET应用程序,您可以尝试我使用的解决方案来捕获所有未处理的异常。当我不使用调试器时,我通常只启用生产代码(使用#ifndef DEBUG)。

值得注意的是,正如kgiannakakis所提到的,您无法捕获其他线程中的异常,但是您可以在这些线程中使用相同的try-catch方案,并将异常发送回主线程,在那里您可以重新抛出它们以获取完整的堆栈跟踪,了解出现了什么问题。

1
如果您想要恢复被抛出的异常类型,您可以在最后使用 catch (...) 前,按照从特定到更一般的顺序链接特定类型的 catch 块来实现。这样做可以让您访问和识别任何被调用的 catch。
try {
   ...
} catch (const SomeCustomException& e) {
   ...
} catch (const std::bad_alloc& e) {
   ...
} catch (const std::runtime_error& e) {
   // Show some diagnosic for generic runtime errors...
} catch (const std::exception& e) {
   // Show some diagnosic for any other unhandled std::exceptions...
} catch (...) {
   // Fallback for unknown errors.
   // Possibly rethrow or omit this if you think the OS can do something with it.
}

请注意,如果您发现自己在多个地方都要这样做,并且想要合并代码(可能是为了多个独立程序而有多个main函数),您可以编写一个函数:
void MyExceptionHandler() {
   try {
      throw; // Rethrow the last exception.
   } catch (const SomeCustomException& e) {
      ...
   }
   ...
}

int main(int argc, char** argv) {
   try {
      ...
   } catch (...) {
      MyExceptionHandler();
   }
}

既然你捕获了一个未知的异常,你打算怎么处理它? - Piskvor left the building
@Piskvor:如果你已经耗尽了所知道(或关心的)所有异常类型,那么只能显示“未知内部错误”消息并终止。 - jamesdlin
@jamesdlin: ...如果没有try块也会发生,那为什么还要麻烦呢? - Piskvor left the building
2
@Piskvor:我想是这样,但应用程序仍然可以提供比默认错误消息更友好的消息,这些消息很可能充满术语。它甚至可以包括支持说明。 - jamesdlin
1
捕获并使用EXIT_FAILURE是一个不好的想法。一些操作系统提供了额外的调试异常的工具,可以逃离main()函数。捕获并重新抛出异常。任何已经传播到这里的异常都没有合理的潜力被纠正(如果有的话,在到达这里之前就已经被纠正了)。 - Martin York
显示剩余4条评论

0
一个 catch-all 并不是非常有用,因为你无法查询任何类型/对象信息。然而,如果你可以确保应用程序引发的所有异常都派生自一个基本对象,那么你可以使用一个针对基本异常的 catch 块。但这样就不再是 catch-all 了。

抱歉,我完全误读了你的回答 - 我会删除这条评论。 - anon

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