【问题标题】:C++ exception handling and error reporting idiomsC++ 异常处理和错误报告习惯用法
【发布时间】:2010-09-24 00:15:21
【问题描述】:

在 C++ 中,RAII 通常被认为是处理异常的一种优越方法:如果抛出异常,则解除堆栈,调用所有析构函数并清理资源。

但是,这会导致错误报告出现问题。假设一个非常通用的函数失败,堆栈被展开到顶层,我在日志中看到的只是:

无法从套接字读取:连接被对等方重置。

...或任何同样通用的消息。这并没有说明引发异常的上下文。特别是如果我正在运行类似事件队列处理循环的东西。

当然,我可以用 try/catch 块包装对套接字读取的每个调用,捕获异常,用更详细的上下文信息构造一个新的异常并重新抛出它,但这违背了拥有 RAII 的目的,并且是缓慢但肯定会比处理返回错误代码更糟糕。

在标准 C++ 中详细报告错误的更好方法是什么?我也愿意接受涉及 Boost 的建议。

【问题讨论】:

  • 这听起来不像是 RAII 的问题,而更像是 C++ 异常对象不提供任何固有上下文的问题;换句话说,您无法在捕获点处获得显示异常最初抛出位置的堆栈跟踪。
  • 好吧,既然您对 Boost 持开放态度,请使用Boost.Exception
  • @GMan 是的,我知道这个库,但它无助于添加中间上下文(我已经可以用标准 C++ 告诉一个 throw 站点和一个 catch 站点)。
  • @Alex B:从某种意义上说,如果你使用 catch-and-throw 选项,那么它会让你的 catch 子句更容易编写。您可以将更多上下文信息填充到现有异常中,而不是构建新异常。
  • 您的问题将错误处理中的错误报告和资源管理问题混为一谈。 RAII 仅与资源管理有关。使用异常而不是返回码是为了确保错误能够持续传播,并且函数可以更频繁地返回其语义“结果”。如果您充分利用 RAII,即使在通过返回码处理错误的代码中也会释放资源。通过异常更容易实现这一点,因为您不会意外忘记带有返回码的函数。

标签: c++ exception-handling


【解决方案1】:

正如 James McNellis 在这里所建议的,有一个非常巧妙的技巧,涉及一个守卫对象和 std::uncaught_exception 设施。

想法是这样写代码:

void function(int a, int b)
{
  STACK_TRACE("function") << "a: " << a << ", b: " << b;

  // do anything

}

并且仅在实际抛出异常的情况下记录消息。

类很简单:

class StackTrace: boost::noncopyable // doesn't make sense to copy it
{
public:
  StackTrace(): mStream() {}

  ~StackTrace()
  {
    if (std::uncaught_exception())
    {
      std::cout << mStream.str() << '\n';
    }
  }

  std::ostream& set(char const* function, char const* file, unsigned int line)
  {
    return mStream << file << "#" << line << " - " << function << " - ";
  }

private:
  std::ostringstream mStream;
};

#define STACK_TRACE(func)                           \
  StackTrace ReallyUnwieldyName;                    \
  ReallyUnwieldyName.set(func, __FILE__, __LINE__)

可以使用__PRETTY_FUNC__ 或等效的名称来避免命名函数,但我在实践中发现它对于我自己的口味来说过于混乱/冗长。

请注意,如果您希望它一直存在到作用域的末尾,则需要一个命名对象,这就是这里的目的。我们可以想出一些棘手的方法来生成唯一标识符,但我从来不需要它,即使在函数内保护更窄的范围时,名称隐藏规则也对我们有利。

如果您将其与ExceptionManager(引发的异常注册自己的地方)结合使用,那么您可以获得对最新异常的引用,并且在记录的情况下,您可以决定在异常本身中设置您的堆栈。这样它就会被what 打印出来,如果异常被丢弃则忽略。

这是一个品味问题。

请注意,在存在ExceptionManager 的情况下,您必须注意并非所有异常都可以用它检索--> 只有您自己制作的异常。因此,您仍然需要采取措施防止 std::out_of_range 和第 3 方异常。

【讨论】:

  • 不错的“把戏”。但是,由于StackTrace s ctor 将始终运行,即使没有错误,也应该确保它尽可能便宜,并且排除(对我而言)在构建时插入字符串流!也许使用捕获 C++0x lambda ` [&]() ` 可以解决问题...
  • 再想一想,问题似乎是uncaught_exception 没有告诉你哪个异常未被捕获,因此如果std::bad_alloc 处于活动状态,使用它可能是一个非常糟糕的主意,这你无法检测到。
  • @Martin:这更像是一个概念证明。正如您所注意到的,在bad_alloc 的情况下,您不想请求任何内存,因此您应该预先分配内存(尽可能多地)。此外,堆栈分配的内存会降低成本,这或多或少排除了std::ostringstream。使用operator(char const* name, T const&amp; value) + ad'hoc 格式可以提高性能,如果不需要,使用 shims 还允许不将项目转换为字符串,当然还有你真正想要执行的确切策略...
  • 在我的回答中,请注意uncaught_exception 告诉您是否在您的StackTrace 对象范围内引发了异常。它告诉您是否存在异常活动,这是暗示应该打印消息的必要但不是充分条件。此代码可能会产生虚假的错误消息。我从来没有使用过 ExceptionManager,所以如果它解决了这个问题,我很想看看如何。
  • @Steve:ExceptionManager 的想法是用户异常在抛出时将在活动异常槽中“注册”(通常是因为它们共享一个公共基类)。然后,您无需记录,而是将各种跟踪添加到异常本身,这将仅通过what 打印。因此,如果忽略异常,则不会打印任何内容。我不喜欢的是,如果忽略异常,则不会打印任何内容...当然:) 我更喜欢系统跟踪堆栈展开,太多懒惰的catch(...) 适合我的口味。
【解决方案2】:

假设一个非常通用的函数失败,堆栈被展开到顶层,我在日志中看到的只是 [...]
在标准 C++ 中详细报告错误的更好方法是什么?

错误处理不是类或库本地的 - 它是应用程序级别的问题。

我能回答你的问题是,错误报告应该始终通过首先查看错误处理来实现。 (错误处理还包括用户对错误的处理。)错误处理是关于必须对错误采取什么措施的决策。

这就是为什么错误报告是应用程序级别的问题并且很大程度上取决于应用程序工作流程的原因之一。在一个应用程序中,“对等点重置连接”是一个致命错误 - 在另一个应用程序中,这是一种常态,错误应该被静默处理,连接应该重新建立并重试挂起的操作。

因此,您提到的方法 - 捕获异常,构造一个具有更详细上下文信息的新异常并重新抛出它 - 也是一种有效的方法:它取决于顶级应用程序逻辑(甚至用户配置)来决定错误是真的错误还是必须根据条件采取一些特殊的(重新)操作。

您遇到的是所谓的离线错误处理(也称为异常)的弱点之一。而且我不知道有什么方法可以做得更好。异常会在应用程序中创建辅助代码路径,如果错误报告至关重要,则辅助代码路径的设计必须像主代码路径一样对待。

外联错误处理的明显替代方案是内联错误处理 - 好的 ol' 返回码和错误情况的日志消息。这允许应用程序将所有低严重性日志消息保存到内部(循环)缓冲区(大小固定或可配置)中,并仅在发生高严重性错误时将它们转储到日志中。通过这种方式可以获得更多的上下文信息,并且不同的应用层不必主动了解彼此。这也是安全和关键任务软件等应用领域的标准错误报告方式(有时是字面意义上的“标准”——法律规定),不允许遗漏错误。

【讨论】:

    【解决方案3】:

    我实际上从未这样做过,但您可以滚动自己的“stacktrace”:

    struct ErrorMessage {
        const char *s;
        ErrorMessage(const char *s) : msg(s) {}
        ~ErrorMessage() { if (s) std::cout << s << "\n"; }
        void done() { s = 0; }
    };
    
    void someOperation() {
        ErrorMessage msg("Doing the first bit");
        // do various stuff that could throw
        msg = "Doing the second bit";
        // do more stuff that could throw
        msg.done();
    }
    

    您可以有多个级别(尽管不一定在每个级别都使用它):

    void handleFoo() {
        ErrorMessage msg("Handling foo event");
        someOperation();
        msg.done();
    }
    

    并添加更多的构造函数和成员:

    void handleBar(const BarEvent &b) {
        ErrorMessage msg(std::stringstream("Handling bar event ") << b.id);
        someOperation();
        msg.done();
    }
    

    而且您无需将消息写入std::cout。它可能是某个日志记录对象,并且该对象可以将它们排队,并且除非捕获站点告诉它,否则实际上不会将它们输出到日志中。这样,如果您捕获到不需要记录的异常,则不会写入任何内容。

    它并不漂亮,但它比 try/catch/throw 或检查返回值更漂亮。如果您忘记在成功时调用done(例如,如果您的函数有多个返回而您错过了一个),那么您至少会在日志中看到您的错误,这与资源泄漏不同。

    [编辑:哦,使用合适的宏,您可以将__FILE____LINE__ 存储在ErrorMessage 中。]

    【讨论】:

    • 显式调用done() 的一种潜在替代方法是让析构函数调用uncaught_exception(),并且仅在存在活动异常时打印(或记录或其他)消息。虽然我知道专家说“使用uncaught_exception() 几乎从来都不是一个好主意”,但我认为这种特殊情况可能没问题,因为它是您真正想要不同行为的少数情况之一,具体取决于析构函数是否被称为正常范围结束或堆栈展开的结果。至少,我在一个爱好项目中做到了这一点,并且没有遇到任何问题。
    • @Steve:当我可能是对的时候,我通常也会很震惊。 :-P
    • @James:在查看了相关的 GOTW 之后,在析构函数中检查 uncaught_exception 出错的情况是,当一些其他析构函数调用其中包含 ErrorMessage 的代码时(例如 flush 函数可能合理地想要登录错误),适当地被 try/catch 包围,然后在堆栈展开期间调用该析构函数。即使flush 没有抛出异常,也会打印flush 函数中的错误消息。我认为这也可以通过在构造函数中检查uncaught_exception 来解决,但我不确定。
    • 这是“固定的”,因为我们已经取消了关于flush 的错误消息,因为我们已经有一个异常需要担心。它仍然不完美,因为它可能对日志显示刷新失败以及第一件事失败很有用。但是我们知道,如果在我们创建 ErrorMessage 时uncaught_exception 为真,那么任何抛出的异常都会被抑制。所以也压制消息,为什么不呢?有趣的是,Sutter 宣称 uncaught_exception 不道德:world.std.com/~swmcd/steven/ms/bugs.html 可能是别有用心
    • 哈哈; GOTW 是在他加入 Microsoft 的 VC++ 团队之前编写的 :-)(而且,VC++ 现在实际上已经正确支持它了)。当我使用它时,它的用途更加有限,没有构造函数或析构函数会做任何会创建ErrorMessage 的事情。我真的不喜欢“仅仅抑制来自 dtor 中抛出的异常的消息”的想法......您可能实际上想要处理异常的信息,因为您可以在 dtor 返回之前处理它。另一种选择是使用 ExceptionHandler 对象来跟踪活动异常。
    【解决方案4】:

    您可以将调用堆栈添加到您的异常中。我不确定这对发布版本有多好,但在调试时就像一个魅力。您可以在异常的构造函数中执行此操作(封装它)。有关起点,请参阅here。这在 Linux 上也是可能的——尽管我不记得具体如何。

    【讨论】:

      【解决方案5】:

      我使用 RAII 和异常,并且在整个代码中只有各种类似单元测试的断言语句 - 如果它们失败,堆栈展开到我可以捕获和处理它们的位置。

      #define APP_ASSERT_MSG(Class,Assertion,szDescription)   \
          if ( !(Assertion) )                                \
          {                                                  \
              throw Class(__LINE__,__FILE__,szDescription);  \
          }
      

      对于我的大多数特定用例,我关心的只是记录调试信息,所以我的异常包含文件和行号以及错误消息(消息是可选的,因为我也有一个没有它的断言) .您可以轻松添加 FUNCTION 或某种类型的错误代码以便更好地处理。

      然后我可以这样使用它:

      int nRet = download_file(...);
      APP_ASSERT_MSG(DownloadException == ERR_OK, "Download failed");
      

      这使得错误处理和报告变得更加容易。

      对于真正令人讨厌的调试,我使用 GCC 的函数检测来保留正在发生的事情的跟踪列表。它运行良好,但会降低应用程序的速度。

      【讨论】:

        【解决方案6】:

        FWIW,我经常做的不是使用异常,而是以标准格式(即:使用宏)进行显式错误处理。例如:

        result = DoSomething();
        CHECK_RESULT_AND_RETURN_ON_ERROR( result );
        

        现在,显然,这在设计方面有很多限制:

        • 您的返回代码可能需要有点统一
        • 代码中充斥着宏
        • 您可能需要许多宏来处理各种检查条件

        不过,正如 Dummy00001 所说,好处是:您可以在发生严重错误时有效地按需生成堆栈跟踪,只需缓存结果即可。我还使用这个范例来记录所有意外的错误情况,因此我可以稍后调整代码以处理“在野外”发生的意外情况。

        那是我的 2c。

        【讨论】:

        • 不幸的是,我使用的是 Boost 和 STL,它们确实会引发异常。所以无异常代码不能很好地使用它。
        • 是的,那里没有很好的解决方案......无论如何,您通常最终都会用 try/catch 块包装 STL/boost 调用。对于我的代码/函数,总体上仍然更喜欢我的解决方案,并且只为意外的库异常提供更高级别的处理程序,但是混合范式可能会变得混乱。
        【解决方案7】:

        这是我在库中处理错误报告的方式(这可能适合也可能不适合您的情况)。

        首先,作为设计的一部分,您需要一个“核心”或“系统”库,所有这些通用逻辑都将位于其中。然后,所有其他库将链接到核心并使用其错误报告 API,因此您的整个系统有一个紧凑的逻辑块来处理这种混乱。

        在核心内部,提供一组日志记录 MACROS,例如“LogWarning”和“LogFatal”,其中包含记录的行为和预期用途 - 例如,LogFatal 应该触发进程的硬中止,但低于“LogError”的任何内容都只是简单的咨询(什么都不做)。宏可以提供“printf”接口,自动将“LINE”、“FILE”和“FUNC”宏作为参数附加到处理错误报告的底层单例对象。

        对于对象本身,我会听从你的。但是,您需要配置“排水管”的公共 API - 例如记录到 stderr、记录到日志文件、记录到 MS 服务日志等。您还希望底层单例是线程安全的、尽可能可重入且非常健壮。

        使用此系统,您可以制作“异常报告”一种更多的排水类型。只需向该错误管理器对象添加一个内部 API,将您记录的消息打包为异常,然后抛出它。然后,用户可以在他们的应用程序中使用 ONE LINE 在您的代码中启用和禁用异常错误行为。您可以在库中围绕第三方或系统代码放置 try-catch,然后在 catch 子句中调用“Log ...”宏以启用干净的重新抛出行为(使用某些平台和编译器选项,您甚至可以捕获诸如段错误使用这个)。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-12-12
          • 1970-01-01
          • 1970-01-01
          • 2014-01-08
          • 1970-01-01
          • 2010-09-10
          相关资源
          最近更新 更多