【问题标题】:How to document all exceptions a function might throw?如何记录函数可能抛出的所有异常?
【发布时间】:2011-05-30 14:40:39
【问题描述】:

如果你有一个可能抛出异常的公共函数,它使用其他(私有或公共)辅助函数也可能抛出异常,我认为你应该记录公共函数可以抛出哪些异常,这包括由辅助函数

类似这样的东西(使用 Doxygen):

/** 
 * @throw Exception ...
 * @throw ExceptionThrownByHelper ...
 * @throw ExceptionThrownByHelpersHelper ...
 */
void theFunction() 
{ 
    helperWhichMayThrowException();
}

helperWhichMayThrowException() 还调用了其他可能引发异常的函数。

为此,您可以:

  1. 递归跟踪所有函数theFunction() 调用并查找该函数抛出的异常。这是一项繁重的工作,当您向帮助程序添加异常时,您可能会忘记在某处记录异常。
  2. theFunction() 中捕获助手抛出的所有异常并转换它们,这样您就可以确定只抛出您指定的异常。但是为什么要使用异常呢?
  3. 不用担心辅助函数抛出的异常,但是您不能对所有异常进行单元测试,因为您不知道公共函数会抛出哪些异常
  4. 有一些工具可以(半)自动列出助手等抛出的所有异常。我查看了 Doxygen 的文档,但没有找到执行此操作的方法。

我想使用选项 4,但我还没有找到好的解决方案,也许使用 Doxygen 可行?或者我只是想记录太多???

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

【问题讨论】:

  • 通常假设某个异常可以被抛出是有意义的,但也可以假设它不会被抛出。例如,考虑std::bad_alloc。您应该始终假设它可能被许多操作抛出,例如动态分配或容器操作,并且您应该使用 RAII 进行防御性编码。但是,这并不意味着您需要为它到处放置处理程序,因为在大多数应用程序中,您极不可能看到它,而且当它确实发生时,您不太可能从中恢复它没有很多麻烦。
  • @James McNellis:好的,对于某些异常,这是有道理的,但是某个助手抛出的 NoPermission 异常呢?
  • 为什么要记录异常?它使所有关于异常的想法都变得毫无用处。另请阅读为什么异常规范无用。
  • 您应该根据输入定义函数的行为,如果这意味着必须记录一些辅助函数异常,那很好。请注意,由于违反函数中的某些不变量而引发的异常可能不需要记录——事实上,在调用其他可能引发的代码之前,最好先assert
  • @ybungalobill:我同意@rve 记录异常。程序其余部分的正确性取决于处理这些异常的系统。

标签: c++ exception documentation doxygen


【解决方案1】:

从根本上说,在现实世界的几乎所有情况下,您所要求的都是不可能的。

记录抛出的异常有两个部分。

1) 简单一点。记录在您的方法中直接引发的异常。您可以手动执行此操作,但这非常费力,并且如果您未能使文档与代码保持同步,则文档会产生误导(可能比根本没有文档更糟糕,因为您只能真正信任您确定的文档是 100% 准确的)。我的AtomineerUtils 加载项使这更容易实现,因为它以最少的工作使代码和文档集保持同步。

2) 不可能的位。记录所有可能“通过”您的方法的异常。这意味着通过您的方法调用的方法的整个子树进行递归,以查看它们可能会抛出什么。为什么不可能?好吧,在最简单的情况下,您将静态绑定到已知方法,因此可以扫描它们以查看它们抛出的内容 - 相当容易。但是大多数情况最终会调用动态绑定的方法(例如,虚拟方法、反射或 COM 接口、dll 中的外部库方法、操作系统 API 等),您无法确定可能会抛出什么(因为您不会知道在您实际在最终用户的 PC 上运行程序之前调用什么 - 每台 PC 都不同,在(例如)WinXP 和 Win7 上执行的代码可能完全不同。或者想象你调用了一个虚拟方法,然后有人添加了一个插件-in 到您的程序中,该程序会覆盖该方法并引发一种新类型的异常)。可靠地处理这种情况的唯一方法是在您的方法中捕获所有异常,然后重新抛出可以精确记录的特定异常 - 如果您不能这样做,那么异常的文档几乎仅限于“通常在您的方法中抛出和通常预期的异常”,使“异常错误”基本上没有记录,并简单地传递到更高级别的未处理异常捕获块。 (正是这种可怕的“未定义”异常行为通常导致使用 catch(...) 的必要性 - 从学术上讲,它是“邪恶的”,但如果你希望你的程序是防弹的,你有时必须使用 catch - 确保意外情况不会影响您的应用程序)。

【讨论】:

  • 我同意第一部分,我希望有一种方法可以自动手动记录每个异常。你对第二部分是正确的,我在问我的问题时没有考虑动态绑定方法。所以基本上为了让你的类防弹,你必须添加一个全部捕获以确保不会发生意外异常?
  • @rve: re bullet-proof: 如果你可以在不知道它实际上是什么的情况下处理任何异常(想象一下读取用户偏好。如果由于任何原因失败,你的设计就是使用默认值. 因此,如果发生任何异常,您只需返回默认值),那么全部捕获是个好主意。但是,如果您无法处理异常(想象一下从磁盘加载关键文件,但除非加载该文件,否则您的代码无法继续),那么一切都没有意义:您如何处理您无法处理的情况处理,尤其是因为您在编译时不知道未处理的异常可能是什么?
  • ...所以这真的取决于每个案例。你应该(你能)处理异常(即使你不知道它是什么),还是应该将异常传递给你的调用者并希望其他人知道如何处理它?这就是异常最大的设计难题——如果您认为这是“别人的问题”,那么它通常会变成一个未处理的异常,最终导致您的整个应用程序崩溃(用户看到崩溃/致命错误,并且通常会丢失未保存的数据)。或者,如果您添加全部捕获,您可能会抑制错误的症状,从而使调试变得困难。
  • ..所以确实没有普遍的“正确”答案 - 它必须根据具体情况进行检查。
  • 我认为跟踪所有抛出的异常的唯一方法是让它们成为方法/函数签名的一部分,就像在 Java 中所做的那样。然后被覆盖的方法不能抛出意外的异常,因为这将涉及更改方法签名。
【解决方案2】:

我想出了以下手动解决方案。基本上我只是从我打电话的成员那里复制@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配置文件中添加*_throwsEXCLUDE_SYMBOLS。这样可以确保这些组不会显示为模块。

然后B::bar() 生成此文档:

void B::bar()
做点别的吧。

例外情况:
FooException
异常:
酒吧异常

【讨论】:

  • 嗯,我不确定这个。如果B::bar() 调用其他五十个方法,这是否意味着您必须复制所有五十个@defgroup。另外,如果两个或多个被调用的方法自己调用同一个方法,不知道这个方法的异常会不会出现两次。
  • 我并不是说这是最好的解决方案。我希望有更好的解决方案(类似于 Java 的已检查异常)或 Doxygen 自动找出方法可能抛出的异常。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-02-04
  • 1970-01-01
  • 1970-01-01
  • 2012-02-12
  • 1970-01-01
相关资源
最近更新 更多