【问题标题】:Should I inherit from std::exception?我应该从 std::exception 继承吗?
【发布时间】:2010-12-12 18:35:29
【问题描述】:

我已经看到至少一个可靠的来源(我使用的一个 C++ 类)建议 C++ 中特定于应用程序的异常类应该继承自 std::exception。我不清楚这种方法的好处。

在 C# 中,从 ApplicationException 继承的原因很清楚:您获得了一些有用的方法、属性和构造函数,并且只需要添加或覆盖您需要的内容。使用 std::exception 似乎您得到的只是一个要覆盖的 what() 方法,您也可以自己创建。

那么使用std::exception 作为我的应用程序特定异常类的基类有什么好处(如果有的话)?有什么好的理由不继承std::exception

【问题讨论】:

标签: c++ exception exception-handling


【解决方案1】:

虽然这个问题已经相当老了并且已经得到了很多回答,但我只想添加一个关于如何在 C++11 中进行正确异常处理的注释,因为我在关于异常的讨论中不断地错过这个:

使用std::nested_exceptionstd::throw_with_nested

它在 StackOverflow herehere 上进行了描述,您可以在代码中获取异常的回溯,而无需调试器或繁琐的日志记录,只需编写适当的异常将重新抛出嵌套异常的处理程序。

由于您可以对任何派生的异常类执行此操作,因此您可以向此类回溯添加大量信息! 你也可以看看我的MWE on GitHub,回溯看起来像这样:

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"

您甚至不需要继承 std::runtime_error 就可以在抛出异常时获得大量信息。

我在子类化中看到的唯一好处(而不仅仅是使用std::runtime_error)是您的异常处理程序可以捕获您的自定义异常并做一些特别的事情。例如:

try
{
  // something that may throw
}
catch( const MyException & ex )
{
  // do something specialized with the
  // additional info inside MyException
}
catch( const std::exception & ex )
{
  std::cerr << ex.what() << std::endl;
}
catch( ... )
{
  std::cerr << "unknown exception!" << std::endl;
}

【讨论】:

    【解决方案2】:

    是的,您应该派生自 std::exception

    其他人回答说std::exception 存在无法向其传递文本消息的问题,但是尝试在投掷点格式化用户消息通常不是一个好主意。相反,使用异常对象将所有相关信息传输到捕获站点,然后该站点可以格式化用户友好的消息。

    【讨论】:

    • +1,好点。无论是从关注点分离还是从 i18n 的角度来看,让表示层构造用户消息肯定会更好。
    • @JohnMGant 和 Emil:很有趣,你能举一个具体的例子来说明如何做到这一点。我知道可以从std::exception 派生并携带异常信息。但是谁将负责构建/格式化错误消息? ::what() 函数还是别的什么?
    • 您将在 catch 站点格式化消息,很可能在应用程序(而不是库)级别。您按类型捕获它,然后探测它以获取信息,然后本地化/格式化适当的用户消息。
    【解决方案3】:

    您应该了解的一个继承问题是对象切片。当您编写throw e; 时,throw 表达式会初始化一个临时对象,称为异常对象,其类型是通过从throw 的操作数的静态类型中删除任何顶级cv 限定符来确定的。这可能不是你所期望的。您可以找到here 的问题示例。

    这不是反对继承的论据,它只是“必须知道”的信息。

    【讨论】:

    • 我认为关键在于“throw e;”是邪恶的,“扔”;没问题。
    • 是的,throw; 是可以的,但不明显你应该写这样的东西。
    • 对于使用 "throw e;" 完成重新抛出的 Java 开发人员来说尤其痛苦
    • 只考虑从抽象基类型派生。通过值捕获抽象基类型会给你一个错误(避免切片);通过引用捕获一个抽象基类型然后尝试抛出它的副本会给你一个错误(再次避免切片。)
    • 你也可以通过添加成员函数raise()来解决这个问题,例如:virtual void raise() { throw *this; }。请记住在每个派生的异常中覆盖它。
    【解决方案4】:

    子类异常的另一个原因是在大型封装系统上工作时更好的设计方面。您可以将它重用于验证消息、用户查询、致命控制器错误等。您可以简单地在主源文件中“捕获”它,而不是重写或重新挂钩所有验证(如消息),但在整个类集中的任何位置抛出错误。

    例如致命异常将终止程序,验证错误只会清除堆栈,用户查询将向最终用户提问。

    这样做还意味着您可以在不同的接口上重用相同的类。例如Windows 应用程序可以使用消息框,Web 服务将显示 html,报告系统将记录它等等。

    【讨论】:

      【解决方案5】:

      std::exception 的问题在于没有接受消息的构造函数(在标准兼容版本中)。

      因此,我更喜欢从std::runtime_error 派生。它派生自 std::exception,但它的构造函数允许您将 C-String 或 std::string 传递给将在调用 what() 时返回的构造函数(作为 char const*)。

      【讨论】:

      • 在抛出时格式化用户友好的消息不是一个好主意,因为这会将低级代码与本地化功能等结合起来。相反,将所有相关信息存储在异常对象中,并让 catch 站点根据异常类型及其携带的数据格式化用户友好的消息。
      • @Emil:当然,如果您是例外,请携带用户可显示的消息,尽管通常它们仅用于记录目的。在 throw 站点上,您没有构建用户消息的上下文(因为无论如何这可能是一个库)。捕获站点将具有更多上下文,并且可以生成适当的消息。
      • 我不知道你从哪里得到异常仅用于记录目的的想法。 :)
      • 异常中不应有文本,只有处理它所需的数据。在处理异常涉及通知用户问题的情况下,异常中的数据需要足以在捕获点格式化友好消息。在抛出时格式化消息是不正确的,因为 1) 可能没有足够的信息来执行此操作,并且 2) 可能必须将其翻译成不同的语言。
      • @Emil:我们已经讨论过这个论点。我指出这不是您在例外中提出的内容。您对主题的理解似乎很好,但您的论点有缺陷。为用户生成的错误消息与为开发者生成的日志消息完全不同。
      【解决方案6】:

      是否从任何标准异常类型派生是第一个问题。这样做可以为所有标准库异常和您自己的异常处理程序启用一个异常处理程序,但它也鼓励使用这种包罗万象的处理程序。问题是人们应该只捕获知道如何处理的异常。例如,在 main() 中,如果 what() 字符串将在退出之前作为最后的手段被记录,那么捕获所有 std::exceptions 可能是一件好事。然而,在其他地方,这不太可能是一个好主意。

      一旦您决定是否从标准异常类型派生,那么问题是哪个应该是基础。如果您的应用程序不需要 i18n,您可能会认为在调用站点格式化消息与在调用站点保存信息和生成消息一样好。问题是可能不需要格式化的消息。最好使用惰性消息生成方案——也许使用预先分配的内存。然后,如果需要该消息,它将在访问时生成(并且可能缓存在异常对象中)。因此,如果消息在抛出时生成,则需要 std::exception 派生类,如 std::runtime_error 作为基类。如果消息是延迟生成的,则 std::exception 是适当的基础。

      【讨论】:

        【解决方案7】:

        我曾经参与过一个大型代码库的清理工作,以前的作者在该代码库中抛出了 int、HRESULTS、std::string、char*、随机类......到处都是不同的东西;只需命名一个类型,它可能会被扔到某个地方。而且根本没有通用的基类。相信我,一旦我们达到所有投掷类型都有一个共同的基础,我们可以抓住并且知道什么都不会过去,事情就会变得更加整洁。所以请帮自己(以及将来必须维护您的代码的人)一个忙,从一开始就这样做。

        【讨论】:

          【解决方案8】:

          如果您所有可能的异常都源自std::exception,那么您的catch 块可以简单地catch(std::exception &amp; e) 并确保捕获所有内容。

          捕获异常后,您可以使用what 方法获取更多信息。 C++ 不支持鸭子类型,因此具有what 方法的另一个类需要不同的catch 和不同的代码才能使用它。

          【讨论】:

          • 不要继承 std::exception 然后捕获 (std::exception e)。您需要捕获对 std::exception& (或指针)的引用,否则您将切掉子类数据。
          • 现在这是一个愚蠢的错误——我当然知道得更清楚。 stackoverflow.com/questions/1095225/…
          【解决方案9】:

          主要的好处是使用你的类的代码不必知道你throw 的确切类型,而可以只知道catchstd::exception

          编辑: 正如 Martin 和其他人所指出的,您实际上希望从 &lt;stdexcept&gt; 标头中声明的 std::exception 的子类之一派生。

          【讨论】:

          • 无法将消息传递给 std::exception。 std::runtime_error 接受一个字符串并从 std::exception 派生。
          • 您不应该将消息传递给异常类型构造函数(考虑到消息需要本地化。)相反,定义一个对错误进行语义分类的异常类型,在异常对象中存储您需要格式化用户友好的消息,然后在捕获站点进行。
          • std::exception&lt;exception&gt; 标头 (proof) 中定义-1
          • 那你的意思是什么?定义意味着声明,您认为这里有什么问题?
          【解决方案10】:

          您应该从boost::exception 继承。它提供了更多的功能和易于理解的方式来携带额外的数据......当然,如果你没有使用Boost,那么请忽略这个建议。

          【讨论】:

          • 但是请注意 boost::exception 本身并不是从 std::exception 派生的。即使从 boost::exception 派生,您也应该从 std::exception 派生(作为单独说明,无论何时从异常类型派生,您都应该使用虚拟继承。)
          【解决方案11】:

          由于该语言已经抛出 std::exception,因此您无论如何都需要捕获它以提供体面的错误报告。您也可以对自己的所有意外异常使用相同的捕获。此外,几乎所有抛出异常的库都会从 std::exception 派生它们。

          换句话说,它要么

          catch (...) {cout << "Unknown exception"; }
          

          catch (const std::exception &e) { cout << "unexpected exception " << e.what();}
          

          第二个选择肯定更好。

          【讨论】:

            【解决方案12】:

            std::exception 继承的原因是它是异常的“标准”基类,因此团队中的其他人很自然地会期待并捕获基类std::exception

            如果您正在寻找方便,您可以从提供std::string 构造函数的std::runtime_error 继承。

            【讨论】:

            • 从除 std::exception 之外的任何其他标准异常类型派生可能不是一个好主意。问题是它们非虚拟地从 std::exception 派生,这可能导致涉及多重继承的微妙错误,其中 catch(std::exception&) 可能由于转换为 std::exception 而默默地无法捕获异常模棱两可。
            • 我阅读了关于提升主题的文章。从纯粹的逻辑意义上来说,这似乎是合理的。但是我从来没有在野外看到过MH。我也不会考虑在异常中使用 MH,所以它看起来像是一个大锤来解决一个不存在的问题。如果这是一个真正的问题,我相信我们会看到标准委员会采取行动解决如此明显的缺陷。所以我的观点是 std::runtime_error (和家庭)仍然是完全可以接受的抛出或派生的异常。
            • @Emil:从std::exception 之外的其他标准异常派生是一个优秀 的想法。你不应该做的是从一个更多继承。
            • @MooingDuck:如果您从一种以上的标准异常类型派生,您最终会导致 std::exception 被多次派生(非虚拟地)。见boost.org/doc/libs/release/libs/exception/doc/…
            • @Emil:这就是我所说的。
            【解决方案13】:

            Difference: std::runtime_error vs std::exception()

            你是否应该继承它取决于你。标准std::exception 及其标准后代提出了一种可能的异常层次结构(分为logic_error 子层次结构和runtime_error 子层次结构)和一种可能的异常对象接口。如果你喜欢它 - 使用它。如果出于某种原因您需要不同的东西 - 定义您自己的异常框架。

            【讨论】:

              【解决方案14】:

              您可能想要从std::exception 继承的原因是因为它允许您抛出根据该类捕获的异常,即:

              class myException : public std::exception { ... };
              try {
                  ...
                  throw myException();
              }
              catch (std::exception &theException) {
                  ...
              }
              

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2011-12-30
                • 2011-04-21
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2011-09-22
                相关资源
                最近更新 更多