返回介绍

7.2 现实中的问题

发布于 2024-08-19 12:44:37 字数 4844 浏览 0 评论 0 收藏 0

当然总有一些应用不适合使用异常,例如:

  • 内存严重受限系统,异常处理所需的运行期支持内存会占用应用程序功能所需要的内存。
  • 工具链不能保证异常抛出后能够迅速做出响应的硬实时系统(例如 [Lockheed Martin Corporation 2005])。
  • 系统依赖于多台不可靠的计算机,因此立即崩溃并重新启动是对付那些无法在本地处理的错误的合理(且几乎是必要的)方式。

因此,大多数 C++ 实现仍然保留了非异常机制的错误处理方式。另一方面,也存在一些通过错误码无法提供良好解决方案的场景:

  • 构造函数失败——由于构造函数没有返回值(不算被构造对象本身),单纯依赖 RAII 的方式必须替换为通过对对象状态的显式检查来处理错误。
  • 运算符——没有办法从 ++*-> 中返回错误码。你将不得不使用非本地的错误指示,或使用一种糟糕的写法,如 multiply(add(a,b),c) 而不是 (a+b)*c[1:1]
  • 回调——使用回调的函数应该能够调用具有多种可能错误的函数(通常,回调是 lambda 表达式(§4.3.1))。
  • 非 C++ 代码——我们无法通过那些没有专门做错误码处理的非 C++ 函数传递错误。
  • 调用链深处新的错误类型——必须准备调用链上的每个函数来处理或传播一种新的错误(例如,程序中的网络错误,而它并不是专门为通过网络访问数据而预先设计的)。
  • 忘记处理返回码——有一些精巧的方案来试图确保统一检查错误码,但是它们要么不完整,要么依赖于在遗漏检查时使用异常或程序终止(例如 [Botet and Bastien 2018])。

此外,还有一些与使用异常有关的现实问题:

  • 有些人不愿意引入异常机制,是因为他们的代码由于无原则使用指针而形成了一团乱麻。通常,这些人将他们的批评指向异常,而不是他们的陈旧代码。
  • 有些人(很多)根本不理解甚至不知道 RAII(§2.2),而只是把异常当作返回错误码的一种替代机制来用。通常,把 try-catch 当作 if-then 的一种形式来用的话,代码比正确使用错误码或 RAII 要更丑陋、更繁琐、更缓慢。
  • 异常的许多实现速度很慢,是因为实现者把 C++ 的异常与其他类型的异常(例如微软的结构异常)统一处理,优先考虑调试(例如 GCC 在 throw 后两次遍历堆栈来保存回溯),使用单一机制为各种语言服务(每一种都很糟糕),或者只是没有在异常处理优化上花费很多开发精力。
  • 这些年来,异常处理的性能相对较慢,是因为我们在优化非异常方面花费了大量精力。我怀疑还有很大的优化机会。 例如,Gor Nishanov 报告说,通过一些与 Windows 和 Linux 上的协程实现相关的简单优化,速度提高了多达 1000 倍 [Nishanov 2019a]。不过,大幅改善空间占用可能会更难实现。一些最近的实验看起来还比较不错 [Renwick et al. 2019]。
  • 为了使异常被接受,我们不得不添加了异常规约 [Stroustrup 2007]。但异常规约从来没有提供支持者们所声称的更好的可维护性,而确实提供了反对者(包括我)所诟病的冗长和开销。一旦异常规约出现在语言中,许多 人就觉得使用它们是受到鼓励的,并将由此产生的问题归咎于异常机制本身。具有讽刺意味的是,那些坚定支持异常规约的人转而去帮助设计 Java 了。异常规约在 2010 年被宣布废弃,并最终在 2017 年被移除(§4.5.3)。作为部分替代方案,C++11 引入了 noexcept 作为一种更简单、更有效的控制异常的机制(§4.5.3)。
  • 通过指定要捕获的异常类型来捕获异常往往使 throwcatch 的实现与运行期类型识别(RTTI [Stroustrup 2007])纠缠在一起,这导致了效率低下和复杂性。特别是,它会导致内存被消耗(被 RTTI 所需的数据消耗),即使应用程序从不依赖 RTTI 来区分异常,对于简单的场景也很难做优化。而且,依赖 RTTI 使得使用动态链接的类型匹配很难优化。基本上,异常处理实现是针对罕见的最复杂的情况进行优化的。当一个具有嵌套异常的类被添加到标准库中,人们甚至被鼓 励在最简单的情况下使用它时,情况就更糟了。对于可以静态分析的类层次结构(在许多嵌入式系统中),以常量时间进行快速类型匹配是可能的 [Gibbs and Stroustrup 2006]。由于异常是平台 ABI 的一部分,这就使得要改变早期的过度设计非常之困难。
  • 有人坚持只使用一种错误处理方法,并且通常得出这样的结论:由于异常不适用于每种情况,因此该方法必须是错误码。那些由错误码所带来的问题也就仅仅是不方便而已
  • 一些人相信那些关于异常机制的基于最坏情况和/或不切实际的比较的低效传闻,例如在添加异常后保留错误码处理方式,将不完整的错误处理与基于异常 的处理进行比较,或者使用异常来做简单的错误处理,而不是把异常用于无法在本地处理的错误。很少有关于异常及其替代方案成本的认真调查。我怀疑关于异常的 虚假传说比任何事实都具有更大的影响力。

最终结果是 C++ 社区分裂为异常和非异常阵营。事实上,不要异常是一种方言,而方言是标准要避免的事情之一(§3.1)。对于个人组织或社区而言,方言可能有一些优势,但它使代码和技能的共享变得复杂,因此损害了整个 C++ 社区。

有人声称,异常机制的问题在于它违反了零开销原则(例如 [Sutter 2018b])。对比通过终止应用来响应错误的处理方案,任何错误处理机制显然都是开销,也都违反了零开销原则(除非考虑到处理终止的成本,例如在另一个 处理器中)。在我们设计异常时,我们考虑了这些,并认为开销是可接受的。理由是:异常情况很少见;除非抛出异常,否则没有运行期开销;并且用于实现异常的 表可以保存在虚拟内存中 [Koenig and Stroustrup 1989]。在虚拟内存不可用或内存不足的情况下,使用表来实现异常可能成为一个严重问题。我们当时设计异常时主要关注的是,需要某种形式的错误传播和错 误处理的系统。在这种情况下,零开销可以解释为异常与以在同样严格程度的错误处理下的错误码使用相比没有额外开销

如今,错误处理的混乱比以往任何时候都严重,处理错误的替代技术比以往任何时候都多,从而造成很大的混乱和危害。假设有 N 种错误处理方式,又有人提出了一个新的解决方案,只要旧的解决方案不被抛弃,现在我们就必须应对 N+1 种方式(N+1 问题)。 如果一个组织有 M 个程序,使用了 N 个库,我们甚至可能有 N*M 个需要处理的问题。异常的引入可以看作是将处理错误的常用方法从 7 种增加到了 8 种。2015 年,Lawrence Crowl 撰写了一份问题分析报告 [Crowl2015a] 对这个问题进行了分析。

基础库的作者对多种错误处理方案的问题感受最为深刻。他们不知道他们的用户喜欢什么,他们的用户可能有很多不同的偏好。C++17 文件系统库(§8.6)的作者们选择了把接口重复一遍:对于每个操作,他们提供两个函数,一个在错误的情况下抛出异常,另一个函数则通过设置标准库的 error_code 参数将错误码通过参数传递出来:

bool create_directory(const filesystem::path& p); // 出现错误时抛异常
bool create_directory(const filesystem::path& p, error_code& ec) noexcept;

当然,这有点冗长,只会取悦那些仅喜欢异常或 error_code 的人。也要注意作者提供了 bool 返回值,这样人们就不必一直使用 try 或直接测试 error_code 了。事实上,文件系统(在我看来相当正确)使用异常来处理罕见的错误并不能让那些认为异常有根本缺陷的人满意,特别是,它仍要求存在异常支持。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文