10

我在使用 C++ 代码时遇到了一些问题,调用者出乎意料地抛出了异常。阅读您正在使用的模块的每一行以查看它是否抛出异常以及如果是,那么是什么类型的异常并不总是可能或实际的。

是否存在处理此问题的既定习语或“最佳实践”?

我想到了以下几点:

  1. 在我们的 doxygen 文档中,我们可以在每个预期会引发异常的函数中添加注释及其类型。

    • 优点:简单。
    • 缺点:受用户错误的影响。
  2. 为了安全,我们可以有一个应用程序范围try/catch(...)

    • 优点:我们不会再有任何未捕获的异常。
    • 缺点:异常在距离抛出很远的地方被捕获。很难弄清楚该做什么或出了什么问题。
  3. 使用异常规范

    • 优点:这是处理这个问题的语言认可的方式。
    • 缺点:需要重构问题库以使其有效。在编译时未强制执行,因此违规会变成运行时问题,这是我要避免的!

这些方法的任何经验,或我不知道的任何其他方法?

4

11 回答 11

9

对标题问题的简短回答 - 表示函数可以抛出的习语不是记录它“这个函数不会抛出”。也就是说,默认情况下,一切都可以抛出。

C++ 不是 Java,也没有经过编译器检查的异常。C++ 中没有任何内容可以让编译器告诉您您的代码声称它不会抛出,但会调用可能会抛出的东西。所以你不能完全避免这是一个运行时问题。静态分析工具可能会有所帮助,但不确定。

如果您只关心 MSVC,您可以考虑使用空异常规范或__declspec(nothrow)不抛出的函数以及抛出throw(...)的函数。这不会导致代码效率低下,因为 MSVC 不会发出代码来检查声明为 nothrow 的函数实际上没有抛出。GCC 可以做同样的事情-fno-enforce-eh-specs,检查你的编译器文档。然后一切都会自动记录下来。

选项 2,应用程序范围的 try-catch 并不是真正“为了安全”,这只是因为您认为您可以对异常做一些更有用的事情(比如打印出一些东西并干净地退出),而不仅仅是让 C++ 运行时调用terminate。如果您在编写代码时假设某些东西不会抛出,并且它实际上会抛出,那么您可能在其中任何一个实际发生之前就已经未定义,例如,如果析构函数对一致状态做出错误假设。

我通常会做 (1) 的变体:为每个函数记录它提供的异常保证 - nothrow、strong、weak 或 none。最后一个是bug。第一个是珍贵的,但很少见,只有交换函数和析构函数才需要良好的编码。是的,它会受到用户错误的影响,但是任何带有异常的 C++ 编码方式都会受到用户错误的影响。然后最重要的是,如果它可以帮助您执行(1),也可以执行(2)和/或(3)。

Symbian 有一个标准的 C++ 方言,具有一种称为“离开”的机制,在某些方面类似于异常。Symbian 中的约定是,任何可能离开的函数都必须以 L 结尾:CreateLConnectL等。平均而言,这减少了用户错误,因为您可以更容易地看到您是否正在调用可能离开的东西。正如您所料,讨厌它的人讨厌匈牙利符号的应用程序,如果几乎所有功能都离开它,它就不再有用了。正如您所料,如果您确实编写了一个名称中没有 L 的函数,那么在您找出问题之前,它可能会在调试器中很长时间,因为您的假设会使您远离实际的错误。

于 2009-08-11T17:06:21.843 回答
7

解决问题的惯用方法不是表明你的代码可以抛出异常,而是在你的对象中实现异常安全。该标准定义了几个异常保证对象应该实现:

  • 不抛出保证:函数永远不会抛出异常
  • 强大的异常安全保证:如果抛出异常,对象将保持初始状态。
  • 基本异常安全保证:如果抛出异常,对象将保持在有效状态。

当然,该标准记录了每个标准库类的异常安全级别。

这确实是在 C++ 中处理异常的方法。与其标记哪些代码可以或不能抛出异常,不如使用 RAII 来确保您的对象得到清理,并考虑在您的 RAII 对象中实现适当级别的异常安全性,以便它们能够在没有特殊处理的情况下生存抛出异常。

只有当它们允许您的对象处于无效状态时,异常才会真正引起问题。那不应该发生。您的对象应始终至少实现基本保证。(并且实现一个提供适当异常安全级别的容器类是一个很有启发性的 C++ 练习;))

至于文档,当您能够确定某个函数可能会抛出哪些异常时,请随时记录它。但一般来说,当没有指定其他内容时,假定函数可能会抛出异常。空抛出规范有时用于记录函数何时从不抛出。如果它不存在,则假设该函数可能会抛出。

于 2009-08-11T23:59:05.630 回答
6

坦率地说,几乎任何 C++ 函数都可能引发异常。你不应该太担心记录这一点,而是通过使用诸如 RAII 之类的习惯用法来确保你的代码异常安全。

于 2009-08-11T16:49:42.097 回答
2

我同时使用 1 和 2。

关于 1:您无法避免或防止用户错误。如果您认识他,您可以击败没有正确编写 doxygen 的开发人员。但是你无法避免或防止用户错误,所以放弃偏执。如果用户犯了错误,那是他做的,而不是你做的。

关于 2:C# 内置了一种捕获未处理异常的方法。所以这不是一件“坏事”,尽管我同意它闻起来很香。有时,最好是崩溃而不是不一致地运行,但我做了一个练习,记录任何未处理的异常,然后崩溃。这允许人们向我发送日志,以便我可以检查堆栈跟踪并追踪问题。这样,在每次更正之后,发生的崩溃越来越少。

于 2009-08-11T16:53:39.920 回答
2

C++,在 c++11 之前,定义了一个 throw 规范。看到这个问题。我过去的经验是,微软编译器忽略了 C++ 抛出规范。“检查异常”的整个概念是有争议的,尤其是在 Java 领域。许多开发人员认为这是一个失败的实验。

于 2009-08-11T17:13:06.183 回答
2

文档似乎是我所知道的唯一合理的方法。

关于异常规范,Herb Sutter 发表了一篇关于该主题的旧文章(2002 年)http://www.ddj.com/cpp/184401544它讨论了为什么该语言的这个特性没有给我们编译时安全性并最终导致结论是:

所以这似乎是我们作为一个社区到今天为止学到的最好的建议:

道德#1:永远不要编写异常规范。

道德#2:除了可能是一个空的,但如果我是你,我什至会避免。

于 2009-08-11T22:44:14.360 回答
1

使用 doxygen 文档来描述您的方法。当您使用它们时,您需要查看此文档以查看它们的参数是什么,以及它们抛出了哪些异常。

编写单元测试以在抛出异常的情况下运行您的代码。

于 2009-08-11T22:10:07.657 回答
1
  1. 记录函数保证的异常安全级别。正如史蒂夫杰索普在他的回答中指出的那样,有很多级别。理想情况下,如果所有接口都记录了它们,或者至少是必要的最小值:a)从不抛出或 b)可能抛出

    阅读Herb Sutter解释的Abrahams 异常安全保证

  2. 我强烈怀疑它在大型代码库中是否实用。关于远离抛出并且很难弄清楚要做什么的问题,一个好的规则是只在你想要处理异常的地方捕获,否则让它冒泡。异常一出现就捕获并消除它们并不是一个好主意,只是因为……它们是异常,所以你觉得你必须对它做点什么。在复杂的系统中,最好有日志机制,这样更容易跟踪问题。

  3. 不要这样做。阅读 Herb Sutter 的A Pragmatic Look at Exception Specifications和相关文章。

于 2010-02-05T23:27:47.363 回答
0

您应该始终期待异常并处理它们。不幸的是,计算机无法自动为您进行尽职调查。

于 2009-08-11T17:04:53.080 回答
0


,唯一常见的就是表明你不扔任何东西。
然后您还应该手动确保没有异常可以真正逃脱您的方法/功能。注意:如果异常确实逃脱了方法,应用程序将被终止。

#include <stdexcept>

class MyException: public std::exception
{
    public:
    ~MyException() throw() {}
     char* what() const throw()
     {
        try
        {
            // Do Stuff that may throw
            return "HI";
        }
        // Exceptions must not escape this method
        // So catch everything.
        catch(...)
        {
           return "Bad Stuff HAppening Cant Make Exception Message.";
        }
     }
};
于 2009-08-11T23:33:17.023 回答
-1

有一种方法可以指定函数可以在 C++ 中抛出哪些异常。取自 Stroustrup 书中的一个例子是

void f(int a) throw (x2, x3);

它指定 f 只能抛出 x2 或 x3 类型或派生类型的异常。使用这种语法,您可以轻松查看函数声明,了解合法允许抛出哪些异常,并进行相应的编码。

于 2009-08-12T00:59:58.273 回答