如果您在"我们不使用例外"阵营,那么您如何使用标准库?

Joh*_*ell 59 c++ error-handling exception-handling

注意:我不是在扮演魔鬼的拥护者或类似的东西 - 我只是真的好奇,因为我自己不在这个营地.

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

你如何在"我们不使用例外"的背景下实际处理这个问题?

  • 你是否试图永远不会调用可以投掷的功能?(我无法看到它的规模如何,所以如果是这种情况,我很想知道你是如何实现这一目标的)

  • 你没事吧和标准库中投掷,你把"我们不使用异常"为"我们从来不的异常我们的代码,我们从来没有赶上从异常等的代码"?

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

  • 编辑您的构造函数,它们是否会失败,或者按照惯例使用具有专用init函数的两步构造,该函数可以在失败时返回错误代码(构造函数不能),或者您是否执行其他操作?

编辑在问题开始后1周进行了一些小的澄清......下面的评论和问题中的大部分内容都集中在为什么方面的例外与"别的东西".我的兴趣是不是在这,但是你选择做"别的东西",如何你处理标准库部件是抛出异常?

Kev*_*inZ 42

我会为自己和世界的角落回答.我写了c ++ 14(一旦编译器有更好的支持将是17)延迟关键的财务应用程序,处理庞大的金钱,不能下降.规则集是:

  • 没有例外
  • 没有rtti
  • 没有运行时调度
  • (差不多)没有遗产

内存被合并并预先分配,因此初始化后没有malloc调用.数据结构要么是不朽的,要么是可以复制的,因此几乎不存在析构函数(有一些例外,例如范围保护).基本上,我们正在做C +类型安全+模板+ lambda.当然,通过编译器开关禁用异常.对于STL,它的好部分(即:algorithm,numeric,type_traits,iterator,atomic,...)都是可用的.抛出异常的部分很好地与运行时内存分配部分和半OO部分重合,因此我们可以一次性去掉所有的内容:流,除了std :: array,std :: string之外的容器.

为什么这样?

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

  • 很棒的答案 - 你听起来像一个游戏程序员。根据我自己的经验,很少有异常事件是可恢复的,因此异常处理是丑陋的代码和开销,没有任何实际用途。最好用代码消除异常或弃踢的可能性(因为你还能做什么?)。 (3认同)
  • 接受这个答案是因为它的主题是如何使用标准库,而不是为什么不使用异常. (3认同)
  • 我很感激为这个答案付出的努力. (2认同)

Zou*_*uch 20

在我们的例子中,我们通过编译器禁用异常(例如,-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
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

你可以做

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

  • 谷歌的C++风格指南声明他们不使用[例外](https://google.github.io/styleguide/cppguide.html#Exceptions)并给出推理. (3认同)

abl*_*abl 18

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

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

换句话说,对于许多程序员来说,关闭异常是完全没有使用STL的.

  • 总的来说,我认为强迫自己始终坚持标准并不是一个好主意.标准库的设计考虑到了通用性.并且是普遍的成本 - 编译和运行时间,记忆.因此,每当您确切地知道您需要什么,并且它只是std提供的特定可能性时,您应该寻找替代方案.例如:那里有大量的内存分配器,几乎每一个都比std好.这是因为他们没有要求在每个上下文中都相当好(比如std),但需要非常擅长于一个特定的应用程序. (2认同)

Nia*_*all 10

注意我使用例外...但我不得不这样做.

你是否试图永远不会调用可以投掷的功能?(我无法看到它的规模如何,所以如果是这种情况,我很想知道你是如何实现这一目标的)

这可能是不可行的,至少是大规模的.许多函数可以抛出,避免它们完全削弱您的代码库.

您对标准库抛出是否正常,并且您将"我们不使用异常"视为"我们从不从代码中抛出异常而我们从不捕获其他代码的异常"?

你几乎必须要好......如果库代码要抛出异常并且你的代码不会处理它,则终止是默认行为.

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

这是可能的(在某些项目类型的某些时候很流行); 编译器可以/可能支持这一点,但是您需要查阅他们的文档以了解结果将会是什么和可能是什么(以及在这些条件下支持哪些语言功能).

通常,当抛出异常时,程序将需要中止或以其他方式退出.一些编码标准仍然需要这一点,我想到了JSF编码标准(IIRC).

那些"不使用例外"的人的一般策略

大多数函数都有一组前提条件,可以在调用之前检查它们.检查那些.如果不满足,则不要拨打电话; 回到该代码中的错误处理.对于那些你无法检查的功能,以确保满足前提条件......不多,该程序可能会中止.

您可以考虑避免引发异常的库 - 您在标准库的上下文中询问了这一点,因此这不太合适,但它仍然是一个选项.

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

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

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

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

init()技术的使用通常是有利的,因为它可以链接并且比内部"错误"代码更好地扩展.

同样,这些技术来自不存在异常的环境(例如C).使用诸如C++之类的语言没有例外限制了它的可用性和标准库的广度的有用性.


Dav*_*aim 8

我没有试图完全回答你提出的问题,我只是将google作为代码库的一个例子,它不使用异常作为处理错误的机制.

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

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