如何记录函数可能抛出的所有异常?

38
如果您有一个公共函数,该函数可能会抛出异常,并使用其他(私有或公共)助手函数,这些助手函数也可能会抛出异常,我认为您应该记录公共函数可能抛出的异常情况,包括由助手函数引发的异常
可以使用Doxygen等工具编写类似以下的文档注释:
/** 
 * @throw Exception ...
 * @throw ExceptionThrownByHelper ...
 * @throw ExceptionThrownByHelpersHelper ...
 */
void theFunction() 
{ 
    helperWhichMayThrowException();
}

同时 helperWhichMayThrowException() 也调用其他可能会抛出异常的函数。

你可以选择以下方法:

  1. 递归地跟随所有 theFunction() 调用的函数,并查找该函数抛出的异常。这是一项繁重的工作,当你添加一个 helper 的异常时,你可能会忘记在某个地方记录一个异常。
  2. 捕获 theFunction() 中 helpers 抛出的所有异常并转换它们,以确保只抛出指定的异常。但这样做又有何意义呢?
  3. 不要担心 helper 函数抛出的异常,但这样一来,你就无法对所有异常进行单元测试,因为你不知道公共函数可能抛出哪些异常。
  4. 使用一些工具(半)自动列出所有 helper 等抛出的异常。我查看了 Doxygen 的文档,但没有找到实现的方法。

我想使用第四种方式,但我还没有找到一个好的解决方案,也许可以用 Doxygen 实现呢?或者我只是想过分记录异常?

编辑:也许不是很清楚,但我正在寻找一种方便的方式(最好使用 Doxygen)来记录一个函数可能抛出的所有异常,而不需要手动检查所有 helper 函数。方便的方式包括“不记录所有异常”或“捕获并转换 theFunction() 中的所有异常”。


2
经常情况下,假设某个特定的异常可能会被抛出,但同时也假设它不会被抛出是有意义的。例如,考虑std::bad_alloc。你应该始终假设许多操作可能会抛出它,例如动态分配或容器操作,并且你应该使用RAII进行防御性编码。然而,这并不意味着你需要在所有地方都放置处理程序,因为在大多数应用程序中,你几乎不可能看到它,当它发生时,你很难恢复它。 - James McNellis
1
@James McNellis:好的,对于某些异常来说这是有意义的,但是如果是由一些辅助工具抛出的NoPermission异常呢? - rve
为什么要记录异常?这会使异常的整个概念变得毫无意义。还要阅读为什么异常规范是无用的。 - Yakov Galka
3
你应该根据输入定义函数的行为,如果这意味着需要记录一些辅助函数异常,那么可以这样做。请注意,由于函数中某些不变量被违反而引发的异常可能不需要记录 - 实际上,在调用可能引发异常的其他代码之前进行“assert”检查会更好。 - David Rodríguez - dribeas
5
我同意@rve的建议,记录下异常情况。程序的正确性取决于系统如何处理这些异常。 - David Rodríguez - dribeas
2个回答

17

基本上,在几乎所有实际情况下,你所要求的是不可能的。

文档化抛出异常有两个部分。

1)简单的部分。文档化在你的方法中直接抛出的异常。你可以手动完成这个过程,但是这很费力,如果无法保持文档与代码同步,那么文档就会变得误导性(比没有文档更糟糕,因为你只能相信你确定100%准确的文档)。我的 AtomineerUtils 插件可以使这个过程更容易实现,因为它可以用最少的努力来保持代码和文档注释同步。

2)不可能的部分。记录所有可能“穿过”您的方法的异常。这意味着需要递归遍历整个子树,以查看由您的方法调用的方法可能会抛出什么异常。为什么是不可能的呢?因为在最简单的情况下,您将静态地绑定到已知的方法,并且因此可以扫描它们以查看它们抛出了什么异常-相对容易。但大多数情况下,最终会调用动态绑定的方法(例如虚拟方法、反射或COM接口、DLL中的外部库方法、操作系统API等),因此您无法确定可能抛出什么异常(因为在真正运行程序时,您不知道会调用什么-每台PC都不同,并且在WinXP和Win7上执行的代码可能完全不同。或者想象一下,您调用了一个虚拟方法,然后有人向您的程序添加了一个覆盖该方法并引发新类型异常的插件)。可靠处理这种情况的唯一方法是在您的方法中捕获所有异常,然后重新抛出特定的异常,然后可以准确记录它们-如果您无法做到这一点,则异常文档化就很大程度上局限于您的方法中“通常抛出和通常预期异常”,而将“异常错误”留给更高级别的未处理异常捕获块。 (正是这种可怕的异常“未定义”行为通常导致需要使用catch(...) - 从学术上讲它是“邪恶”的,但如果您想使程序防弹,有时必须使用catch-all,以确保意外情况不会刺杀您的应用程序)。


1
我同意第一部分,并希望有一种方法可以自动化手动记录每个异常。你在第二部分是正确的,我没有考虑到动态绑定方法在问问题时。所以基本上为了使您的类牢固,您必须添加一个catch all以确保不会发生意外的异常? - rve
1
...所以这真的取决于每个个案。你应该(可以)处理异常(即使你不知道它是什么),还是将异常传递给调用者,希望其他人知道如何处理它?这就是异常处理中最大的设计难题 - 如果你假设它是“别人的问题”,那么它通常会变成未处理的异常,最终导致整个应用程序崩溃/致命错误(用户看到崩溃/致命错误,并且通常会失去未保存的数据)。或者,如果你添加了catch alls,你可能会抑制错误的症状,使得调试困难。 - Jason Williams
1
因此,真正的"正确"答案并不存在 - 必须根据具体情况进行审查。 - Jason Williams
1
@Jason Williams:重新传递异常:我同意,但如果您不记录传递的异常,则永远不会捕获它们,因为客户端不知道可能存在哪些异常,并且这些异常无法进行测试,因为不知道可能会抛出什么异常。 - rve
3
我认为追踪所有抛出的异常的唯一方法是将它们作为方法/函数签名的一部分,就像Java中所做的那样。这样,重写的方法就不能抛出意外的异常,因为这将涉及更改方法签名。 - Giorgio
显示剩余2条评论

8

我提出了以下手动解决方案。基本上我只是从我调用的成员中复制 @throw 文档。如果 Doxygen 有类似于 @copydoc@copythrows 就好了,但以下方法也可以:

class A {
    public:
        /** @defgroup A_foo_throws
         *
         * @throws FooException
         */

        /** 
         * @brief Do something.
         *
         * @copydetails A_foo_throws
         */
        void foo();
};

class B {
    public:
        // This group contains all exceptions thrown by B::bar()
        // Since B::bar() calls A::foo(), we also copy the exceptions
        // thrown by A::foo().

        /** @defgroup B_bar_throws
         *
         * @copydetails A_foo_throws
         * @throws BarException
         */

        /**
         * @brief Do something else.
         *
         * @copydetails B_bar_throws
         */
        void bar();
};  

然后在Doxyfile配置文件中将*_throws添加到EXCLUDE_SYMBOLS。这样可以确保这些组不会显示为模块。

然后B::bar()的文档如下:

void B::bar()
做一些其他事情。

异常:
  FooException
异常:
  BarException


嗯,我对这个不太确定。如果 B::bar() 调用了其他五十个方法,那么是否意味着你需要复制所有的五十个 @defgroup。此外,如果调用的两个或更多方法本身调用相同的方法,我想知道这个方法的异常是否会出现两次。 - Richard-Degenne
我并不是说这是最好的解决方案。我希望有一个更好的解决方案(比如Java中的checked exceptions),或者Doxygen自动找出一个方法可能抛出的异常。 - rve

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