C++异常处理和错误报告习语

Ale*_*x B 15 c++ exception-handling

在C++中,RAII经常被提倡作为异常处理的优秀方法:如果抛出异常,则堆栈被展开,所有析构函数都被调用,资源被清理.

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

无法从套接字读取:由对等方重置连接.

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

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

在标准C++中详细报告错误报告的更好方法是什么?我也对涉及Boost的建议持开放态度.

Mat*_* M. 5

正如詹姆斯麦克纳利斯在这里所说的那样,有一个非常巧妙的技巧涉及一个防守对象和std::uncaught_exception设施.

想法是编写这样的代码:

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

  // do anything

}
Run Code Online (Sandbox Code Playgroud)

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

这堂课非常简单:

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__)
Run Code Online (Sandbox Code Playgroud)

人们可以使用__PRETTY_FUNC__或等同于避免命名该功能,但我在实践中发现它对我自己的品味来说太过错误/冗长.

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

如果将它与ExceptionManager(将抛出的异常自身注册的东西)结合起来,那么您可以获得对最新异常的引用,并且在记录的情况下,您可以决定在异常本身内设置堆栈.因此,what如果异常被丢弃,它将被打印并被忽略.

这是一个品味问题.

请注意,在ExceptionManager您面前,您必须意识到并非所有异常都可以通过它来检索 - >只有您自己制作的异常.因此,您仍需要一定程度的防范std::out_of_range和第三方例外.

  • 不错的"伎俩".但是,由于`StackTrace的ctor将始终运行,即使没有错误,也应该确保它尽可能便宜,并且排除(对我来说)在构造时插入字符串流!也许使用捕获C++ 0x lambda` [&]()`可以做到这一点...... (2认同)

Ste*_*sop 4

我从来没有真正这样做过,但你可以推出你自己的“堆栈跟踪”:

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();
}
Run Code Online (Sandbox Code Playgroud)

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

void handleFoo() {
    ErrorMessage msg("Handling foo event");
    someOperation();
    msg.done();
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

  • 显式调用“done()”的一种潜在替代方法是让析构函数调用“uncaught_exception()”,并且仅在存在活动异常时打印(或记录或其他)消息。虽然我知道专家们说“使用‘uncaught_exception()’几乎从来都不是一个好主意”,但我认为这种特殊情况可能没问题,因为这是少数几次你确实想要根据析构函数是否是不同的行为之一。由于正常作用域结束或堆栈展开而调用。至少,我在一个业余爱好项目中做到了这一点,并且没有遇到任何麻烦。 (3认同)