如果您属于"we don't use exceptions"阵营,那么您如何使用标准库?

If you're in the "we don't use exceptions" camp, then how do you use the standard library?

本文关键字:标准 阵营 何使用 use don we 属于 如果 exceptions      更新时间:2023-10-16

注意:我不是在这里扮演魔鬼代言人或类似的角色——我只是真的很好奇,因为我自己不在这个阵营。

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

在"我们不使用例外"的背景下,您是如何实际处理这一问题的?

  • 您是否试图从不调用可以抛出的函数?(我看不出这会如何扩展,所以如果是这样的话,我很想听听你是如何实现的)

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

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

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

编辑问题开始后1周的小澄清。。。下面评论和问题中的大部分内容都集中在为什么方面的例外与"其他东西"。我感兴趣的不是这一点,而是你选择做"其他事情"时,你如何处理抛出异常的标准库部分?

我将为自己和我的世界角落负责。我写的是c++14(一旦编译器有了更好的支持,它将是17)延迟关键的金融应用程序,这些应用程序处理巨额资金,永远不会宕机。规则集为:

  • 没有例外
  • 无rtti
  • 无运行时调度
  • (几乎)没有遗产

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

为什么要这样做?

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

在我们的情况下,我们通过编译器禁用异常(例如,gcc的-fno-exceptions)。

在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

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

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

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

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

你可以做

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

我还想指出,在询问不使用异常时,有一个关于标准库的更一般的问题:当你处于"我们不使用异常"阵营时,你是否使用标准库?

标准库很重。在一些"我们不使用例外"阵营中,比如许多GameDev公司,使用了更适合STL的替代品——主要基于EASTL或TTL。这些库无论如何都不使用异常,这是因为第八代控制台处理得不太好(甚至根本没有)。对于最先进的AAA生产代码来说,无论如何,异常都太重了,所以在这种情况下这是一个双赢的场景。

换句话说,对于许多程序员来说,关闭异常与根本不使用STL是相辅相成的。

注意我使用异常。。。但我被迫不这么做。

您是否试图从不调用可以抛出的函数?(我看不出这会如何扩展,所以如果是这样的话,我很想听听你是如何实现的)

这可能是不可行的,至少在大规模范围内是这样。许多函数可能会抛出,避免它们完全削弱您的代码库。

您是否同意标准库抛出,并将"我们不使用异常"视为"我们从不从代码中抛出异常,也从不从其他代码中捕获异常"?

你必须接受。。。如果库代码将抛出异常,而您的代码无法处理该异常,则终止是默认行为。

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

这是可能的(在当时,它在某些项目类型中很流行);编译器确实/可能支持这一点,但您需要查阅他们的文档以了解结果(以及在这些条件下支持哪些语言功能)。

通常,当抛出异常时,程序需要中止或退出。一些编码标准仍然需要这一点,JSF编码标准(IIRC)应运而生。

针对那些"不使用异常"的的一般策略

大多数函数都有一组先决条件,可以在调用前检查。检查一下。如果他们没有见面,就不要打电话;返回到该代码中的任何错误处理。对于那些无法检查以确保满足先决条件的功能。。。不多,程序可能会中止。

您可以考虑避免抛出异常的库-您在标准库的上下文中提出了这个问题,所以这不太符合要求,但它仍然是一个选项。

其他可能的策略;我知道这听起来很老套,但选择一种不使用它们的语言。C可以做得很好。。。

。。。我的问题的关键(您和标准库的交互,如果有的话),我很想听听您的构造函数。它们会失败吗?还是按照惯例,使用带有专用init函数的两步构造,该函数可以在失败时返回错误代码(构造函数不能)?或者你的策略是什么?

如果使用构造函数,通常有两种方法用于指示失败;

  1. 设置一个内部错误代码或enum来指示故障以及故障是什么。这可以在对象构造和采取适当措施后进行询问
  2. 不要使用构造函数(或者至少只构造构造函数中不能失败的东西——如果有的话),然后使用某种init()方法来进行(或完成)构造。如果出现某些失败,成员方法可能会返回一个错误

init()技术的使用通常是受欢迎的,因为它可以被链接,并且比内部"错误"代码扩展得更好。

同样,这些技术来自于不存在异常的环境(如C)。毫无例外地使用C++这样的语言限制了它的可用性和标准库的广度。

我不想完全回答您提出的问题,我只举谷歌作为代码库的例子,它不利用异常作为处理错误的机制。

在Google C++代码库中,每个可能失败的函数都返回一个status对象,该对象具有类似ok的方法来指定被调用者的结果
如果开发人员忽略返回的status对象,他们已将GCC配置为编译失败。

此外,从他们提供的少量开源代码(如LevelDB库)来看,他们似乎并没有那么多地使用STL,因此异常处理变得很少。正如提图斯·温特斯在CPPCon的讲座中所说,他们"尊重标准,但不要崇拜它"。

我认为这是一个态度问题。你需要站在"我不在乎某件事是否失败"的阵营中。这通常会产生代码,需要调试器(在客户站点)来找出为什么突然有些东西不工作了。同样,以这种方式进行软件"工程"的潜在人员也不会使用非常复杂的代码。例如,如果它所依赖的所有n个资源都已成功分配(同时对这些资源使用RAII),那么就无法编写代码,这取决于它只被执行这一事实。因此:这样的编码将导致:

  • 用于错误处理的不可管理的代码量
  • 避免执行代码的不可管理的代码量,这依赖于某些资源的成功分配
  • 没有错误处理,因此支持量和开发时间都大大增加

请注意,我谈论的是现代代码、按需加载客户提供的dll以及使用子进程。有许多接口可能会出现故障。我说的不是grep/more/ls/find的替代品。