如果你在“我们不使用异常”阵营,那么你如何使用标准库?

新手上路,请多包涵

注意: 我不是在这里扮演魔鬼的拥护者或类似的角色——我只是真的很好奇,因为我自己并不在这个营地。

标准库中的大多数类型要么具有可以抛出异常的变异函数(例如,如果内存分配失败),要么具有可以抛出异常的非变异函数(例如越界索引访问器)。除此之外,许多自由函数可以抛出异常(例如 operator newdynamic_cast<T&> )。

在“我们不使用异常”的背景下,您 实际上 如何处理这个问题?

  • 您是否试图 从不 调用可以抛出的函数? (我看不出它是如何扩展的,所以如果是这种情况,我很想听听你是如何做到这一点的)

  • 您是否同意标准库抛出并将“我们不使用异常”视为“我们从不从 我们的 代码中 抛出 异常并且我们从不从 其他 代码中 捕获 异常”?

  • 您是否通过编译器开关完全禁用异常处理?如果是这样,标准库的异常抛出部分是如何工作的?

  • 编辑 您的构造函数,它们会失败,还是按照惯例使用带有专用 init 函数的两步构造,该函数可以在失败时返回错误代码(构造函数不能),还是您做其他事情?

编辑 问题开始后 1 周的次要澄清……下面的评论和问题中的大部分内容都集中在例外与“其他”方面的 _原因_。我的兴趣不在于此,但是 您选择做“其他事情”时,您 如何 处理 引发异常的标准库部分?

原文由 Johann Gerell 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 434
2 个回答

我会为我自己和我的世界一角回答。我编写了 c++14(一旦编译器有更好的支持,将是 17)延迟关键金融应用程序,处理巨额资金并且永远不会下降。规则集是:

  • 没有例外
  • 没有rtti
  • 没有运行时调度
  • (几乎)没有继承

内存是池化和预分配的,所以初始化后没有 malloc 调用。数据结构要么是不朽的,要么是可以简单复制的,因此几乎不存在析构函数(也有一些例外,例如范围保护)。基本上,我们在做 C + 类型安全 + 模板 + lambdas。当然,异常是通过编译器开关禁用的。至于 STL,它的好部分(即:算法、数字、type_traits、迭代器、原子……)都是可用的。抛出异常的部分与运行时内存分配部分和半 OO 部分很好地吻合,所以我们可以一次性摆脱所有的麻烦:流、容器,除了 std::array 和 std::string。

为什么要这样做?

  1. 因为像 OO 一样,异常通过将问题隐藏或移动到其他地方来提供虚幻的清洁,并使程序的其余部分更难诊断。当您在没有“-fno-exceptions”的情况下进行编译时,所有干净且表现良好的函数都必须承受失败的怀疑。在代码库的周边进行广泛的健全性检查比让每个操作都失败要容易得多。
  2. 因为异常基本上是具有未指定目的地的远程 GOTO。您不会使用 longjmp(),但可以说异常情况要糟糕得多。
  3. 因为错误代码更优越。您可以使用 [[nodiscard]] 强制调用代码进行检查。
  4. 因为异常层次结构是不必要的。大多数情况下,区分错误是没有意义的,当它发生时,很可能是因为不同的错误需要不同的清理,并且明确地发出信号会更好。
  5. 因为我们要维护复杂的不变量。这意味着有一些代码,无论多么深入人心,都需要有跨国保证。有两种方法可以做到这一点:要么使命令式过程尽可能纯粹(即:确保永远不会失败),要么拥有不可变的数据结构(即:使故障恢复成为可能)。如果您有不可变的数据结构,那么您当然可以有异常,但您不会使用它们,因为您将使用 sum 类型。虽然函数式数据结构很慢,但另一种选择是拥有纯函数并使用无异常语言(如 C、无例外 C++ 或 Rust)来完成。不管 D 看起来多么漂亮,只要它没有清除 GC 和异常,它就是不可选择的。
  6. 您是否曾经像测试显式代码路径一样测试您的异常?那些“永远不会发生”的异常呢?当然你没有,当你真正遇到这些例外时,你就完蛋了。
  7. 我在 C++ 中看到了一些“漂亮”的异常中性代码。也就是说,无论它调用的代码是否使用异常,它都能在没有边缘情况的情况下以最佳方式执行。它们真的很难写,我怀疑如果你想维护所有的异常保证,修改起来很棘手。但是,我还没有看到任何抛出或捕获异常的“漂亮”代码。我见过的所有直接与异常交互的代码都普遍丑陋。编写与异常无关的代码所付出的努力完全使从抛出或捕获异常的蹩脚代码中节省的努力相形见绌。 “美丽”被引用是因为它不是真正的美丽:它通常是僵化的,因为编辑它需要额外的负担来保持异常中立。如果您没有单元测试来故意和全面地滥用异常来触发这些边缘情况,那么即使是“漂亮的”异常中性代码也会腐烂成粪土。

原文由 KevinZ 发布,翻译遵循 CC BY-SA 4.0 许可协议

在我们的例子中,我们通过编译器禁用异常(例如 -fno-exceptions 用于 gcc)。

在 gcc 的情况下,他们使用一个名为 _GLIBCXX_THROW_OR_ABORT 的宏,它被定义为

#ifndef _GLIBCXX_THROW_OR_ABORT
# if __cpp_exceptions
#  define _GLIBCXX_THROW_OR_ABORT(_EXC) (throw (_EXC))
# else
#  define _GLIBCXX_THROW_OR_ABORT(_EXC) (__builtin_abort())
# endif
#endif

(你可以在 libstdc++-v3/include/bits/c++config 在最新的 gcc 版本中找到它)。

然后你就必须处理抛出的异常只是中止的事实。您仍然可以捕获信号并打印堆栈(SO 上有一个很好的答案可以解释这一点),但您最好避免发生这种事情(至少在版本中)。

如果你想要一些例子,而不是像

try {
   Foo foo = mymap.at("foo");
   // ...
} catch (std::exception& e) {}

你可以做

auto it = mymap.find("foo");
if (it != mymap.end()) {
    Foo foo = it->second;
    // ...
}

原文由 cmourglia 发布,翻译遵循 CC BY-SA 3.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题