Boost C++ 库

...世界上最受尊敬和专家设计的 C++ 库项目之一。 Herb SutterAndrei Alexandrescu, C++ 编码标准

错误和异常处理

参考文献

以下论文很好地介绍了编写健壮的泛型组件的一些问题

D. Abrahams: ``泛型组件中的异常安全'',最初发表于 M. Jazayeri, R. Loos, D. Musser (eds.): 泛型编程,Dagstuhl 研讨会论文集,计算机科学讲义。卷。1766

准则

我应该何时使用异常?

简单的答案是:``只要异常的语义和性能特征合适。''

一个经常被引用的准则是问自己这个问题“这是一种异常(或意外)情况吗?”这个准则听起来很有吸引力,但通常是错误的。问题是一个人的“异常”是另一个人的“预期”:当你仔细查看这些术语时,区别就消失了,你就没有了准则。毕竟,如果你检查错误条件,那么在某种意义上你期望它发生,否则检查就是浪费代码。

一个更合适的问题是:``我们这里想要堆栈展开吗?''因为实际处理异常可能比执行主线代码慢得多,你还应该问:``我能负担得起这里的堆栈展开吗?''例如,执行长时间计算的桌面应用程序可能会定期检查用户是否按下了取消按钮。抛出异常可以允许操作正常取消。另一方面,在这种计算的内部循环中抛出和*处理*异常可能是不合适的,因为这可能会对性能产生重大影响。上面提到的准则有一定的道理:在时间关键的代码中,抛出异常应该是*例外*,而不是规则。

我应该如何设计我的异常类?

  1. **从 `std::exception` 派生你的异常类**。除了*非常*罕见的情况下,你无法承受虚函数表的成本,`std::exception` 是一个合理的异常基类,并且当普遍使用时,允许程序员捕获“所有内容”而无需诉诸 `catch(...)`。有关 `catch(...)` 的更多信息,请参见下文。
  2. **使用*虚*继承。**这个见解来自 Andrew Koenig。从异常的基类使用虚继承可以防止在 catch 站点出现歧义问题,以防有人抛出从具有共同基类的多个基类派生的异常
    #include <iostream>
    struct my_exc1 : std::exception { char const* what() const throw(); };
    struct my_exc2 : std::exception { char const* what() const throw(); };
    struct your_exc3 : my_exc1, my_exc2 {};
    
    int main()
    {
       try { throw your_exc3(); }
       catch(std::exception const& e) {}
       catch(...) { std::cout << "whoops!" << std::endl; }
    }
    
    上面的程序打印 `“whoops”`,因为 C++ 运行时无法解析在第一个 catch 子句中匹配哪个 `exception` 实例。
  3. ** *不要*嵌入 `std::string` 对象**或任何其他数据成员或基类,其复制构造函数可能会抛出异常。这可能直接导致在抛出点 `std::terminate()`。类似地,使用其普通构造函数可能抛出的基类或成员也是一个坏主意,因为,虽然不一定会导致程序致命,但你可能会从包含构造的 *throw-expression* 中报告与预期不同的异常,例如
    throw some_exception();    
    

    有多种方法可以避免在复制异常时复制字符串对象,包括在异常对象中嵌入固定长度的缓冲区,或通过引用计数管理字符串。但是,在采用这些方法之前,请考虑下一个要点。

  4. **按需格式化 `what()` 消息**,如果你真的必须格式化消息。格式化异常错误消息通常是内存密集型操作,可能会抛出异常。最好将此操作延迟到堆栈展开发生之后,并且可能释放了一些资源。在这种情况下,最好使用 `catch(...)` 块保护你的 `what()` 函数,以便在格式化代码抛出时有一个回退
  5. **不要*太*担心 `what()` 消息**。有一个程序员有机会弄清楚的消息很好,但是你不太可能能够在抛出异常时编写一个相关的且*用户*可以理解的错误消息。当然,国际化超出了异常类作者的范围。Peter Dimov提出了一个很好的论点,即 `what()` 字符串的正确用法是作为错误消息格式化程序表中的键。现在,如果我们能够获得标准库抛出的异常的标准化 `what()` 字符串就好了……
  6. **在异常类的公共接口中公开与错误原因相关的相关信息**。对 `what()` 消息的关注可能意味着你忽略了公开某人可能需要的信息,以便为用户提供连贯的消息。例如,如果你的异常报告数值范围错误,则必须在异常类的公共接口中将所涉及的实际数字*作为数字*提供,以便错误报告代码可以使用它们进行智能处理。如果你只在 `what()` 字符串中公开这些数字的文本表示,那么你将使需要对它们进行更多操作(例如减法)而不是哑输出的程序员的生活变得非常困难。
  7. **如果可能,使你的异常类免受双重销毁的影响**。不幸的是,一些流行的编译器偶尔会导致异常对象被销毁两次。如果你可以安排使其无害(例如通过将已删除的指针清零),你的代码将更加健壮。

程序员错误怎么办?

作为开发人员,如果我违反了我正在使用的库的前提条件,我不希望堆栈展开。我想要的是核心转储或等效的东西——一种在检测到问题的确切位置检查程序状态的方法。这通常意味着 `assert()` 或类似的东西。

有时需要具有弹性的 API,可以承受几乎任何类型的客户端滥用,但这种方法通常会付出巨大的代价。例如,它通常要求跟踪客户端使用的每个对象,以便可以检查其有效性。如果你需要这种保护,它通常可以作为更简单的 API 之上的一层提供。但是,要提防半途而废的措施。承诺对某些(但不是所有)滥用具有弹性的 API 是灾难的邀请。客户端将开始依赖于保护,并且他们的期望将增长到涵盖接口中未受保护的部分。

**Windows 开发人员注意事项**:不幸的是,大多数 Windows 编译器使用的原生异常处理在使用 `assert()` 时实际上会抛出异常。实际上,这适用于其他程序员错误,例如段错误和被零除错误。这个问题的一个问题是,如果你使用 JIT(即时)调试,在调试器出现之前会有附带的异常展开,因为 `catch(...)` 将捕获这些并非真正的 C++ 异常。幸运的是,有一个简单但鲜为人知的解决方法,那就是使用以下咒语

extern "C" void straight_to_debugger(unsigned int, EXCEPTION_POINTERS*)
{
    throw;
}
extern "C" void (*old_translator)(unsigned, EXCEPTION_POINTERS*)
         = _set_se_translator(straight_to_debugger);

如果从 catch 块(或从 catch 块调用的函数)内部引发 SEH,则此技术不起作用,但它仍然消除了绝大多数 JIT 掩蔽问题。

我应该如何处理异常?

通常,处理异常的最佳方法是根本不处理它们。如果可以让它们通过你的代码并允许析构函数处理清理,你的代码将更干净。

尽可能避免 `catch(...)`

不幸的是,Windows 以外的操作系统也将非 C++“异常”(例如线程取消)缠绕到 C++ EH 机制中,并且有时没有对应于上述 `_set_se_translator` hack 的解决方法。结果是 `catch(...)` 可能会导致在无法恢复的点出现一些意外的系统通知,看起来就像是从合理的地方抛出的 C++ 异常,从而使析构函数和 catch 块已采取有效步骤以确保展开期间程序不变式的通常安全假设失效。

在新闻组中经过多次长时间的辩论后,我不情愿地将这一点让给了 Hillel Y. Sims:在所有操作系统都被“修复”之前,如果每个异常都派生自 `std::exception` 并且每个人都用 `catch(std::exception&)` 替换 `catch(...)`,世界将会变得更美好。

有时,尽管与操作系统/平台设计选择存在不良交互,但 `catch(...)` 仍然是最合适的模式。如果你不知道可能会抛出哪种异常,并且你真的*必须*停止展开它,它可能仍然是你最好的选择。发生这种情况的一个明显地方是语言边界。