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

时间:2016-06-08 10:48:06

标签: c++ error-handling exception-handling

注意:我没有扮演魔鬼的拥护者或类似的东西 - 我真的好奇,因为我不在这个阵营自己。

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

你如何几乎在&#34;我们不使用例外&#34;

的背景下处理这个问题?

  • 你是否试图从不调用可以抛出的函数? (我无法看到它是如何缩放的,所以如果是这种情况,我非常有兴趣听听你如何实现这一目标)

  • 您对标准库投掷是否合适?您对待&#34;我们不使用例外&#34; as&#34;我们从来没有我们的代码中抛出异常,我们从来没有捕获其他异常>代码&#34;?

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

  • 编辑您的构造函数,它们是否会失败,或按惯例执行使用具有专用init函数的两步构造,该函数可在失败时返回错误代码(构造函数可以& #39; t),或者你还做别的什么?

编辑问题开始后1周进行轻微澄清......以下评论和问题中的大部分内容都集中在为什么方面的异常vs&# 34;别的东西&#34;。我的兴趣不在于此,但你选择做什么&#34;其他&#34;,如何你处理的标准库部分抛出异常?

6 个答案:

答案 0 :(得分:41)

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

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

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

为什么这样?

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

答案 1 :(得分:19)

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

(您可以在最新的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;
    // ...
}

答案 2 :(得分:18)

我还想指出,在询问不使用例外情况时,有一个关于标准库的更一般的问题:当你在其中一个“我们穿上”时使用标准库“使用例外”阵营?

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

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

答案 3 :(得分:10)

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

  

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

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

  

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

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

  

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

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

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

“不使用例外”的人的一般策略

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

您可以查看 避免引发异常的库 - 您在标准库的上下文中询问了这一点,因此这不太合适,但它仍然是一种选择。

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

  

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

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

  1. 设置内部错误代码或enum以指示失败以及失败是什么。在对象构造和采取适当的措施后,可以对此进行查询。
  2. 不要使用构造函数(或者至少只构造构造函数中不能失败的东西 - 如果有的话),然后使用某种init()方法来构造(或完成)构造。如果出现故障,成员方法可以返回错误。
  3. init()技术的使用通常受到青睐,因为它可以链接并且比内部“错误”代码更好地扩展。

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

答案 4 :(得分:8)

我没有尝试完全回答您提出的问题,我只是将谷歌作为代码库的示例,它不会将异常作为处理错误的机制。

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

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

答案 5 :(得分:0)

我认为这是一个态度问题。您需要处于“我不在乎是否会失败”的阵营。 这通常会导致产生代码,对于该代码,需要调试器(在客户现场)进行查找,为什么突然有些东西不再起作用了。 同样,以这种方式进行软件“工程”的人们可能不会使用非常复杂的代码。例如。一个人将无法编写代码,这取决于只有在它依赖的所有n个资源都已成功分配(同时使用RAII进行这些资源使用)的情况下,它才被执行。 因此:这样的编码将导致:

  • 大量错误处理代码
  • 大量代码以避免执行代码,这取决于成功分配一些资源
  • 没有错误处理,因此有大量的支持和开发时间

请注意,我正在谈论现代代码,按需加载客户提供的dll,并使用子进程。有许多接口可能会发生故障。我不是说要替换grep / more / ls / find。