Google C ++样式指南的无例外规则;STL?


68

Google的C ++风格指南说“我们不使用例外”。该样式在异常使用方面未提及STL。由于STL分配器可能会失败,因此它们如何处理容器引发的异常?

  1. 如果他们使用STL,如何将分配失败通知呼叫者?STL方法(例如push_back()或map)operator[]不会返回任何状态代码。
  2. 如果他们不使用STL,则使用什么容器实现?

8
在我进来并大喊大叫直到我迷路之前,我工作的那家商店有着同样愚蠢的约定。我们只是忽略了现实。我敢打赌,这也是Google所做的。
爱德华·斯特兰奇

15
Google有相当古老的标准。猜猜他们雇用了很多学生,并且只能负担少量的C ++向导。
Maxim Egorushkin 2011年


21
@Maxim:这就是为什么您需要断言空指针以遵循该建议的原因。如果不这样做的断言,有一个风险,即你的代码可能无法和好如初,因为“未定义行为”确实没有“马上段错误”的意思。公认的风险很小,但请考虑一下例如register.co.uk/2009/07/17/linux_kernel_exploit
Steve Jessop

12
@Maxim:“将页面映射到0地址是自找麻烦。” -的确,攻击者通常在恶意拉扯此类特技时要求麻烦。不过,这只是一个例子。如果您保证可以通过访问空指针而获得段错误,那么编译器将最终通过不保证使您(或类似您的其他人)感到惊讶。程序启动后,未定义的行为可能会及时传播,使恶魔从您的鼻子中飞出。要么尽快失败,要么不失败,但是不要指望失败,然后再失败。
史蒂夫·杰索普

Answers:


56

他们说他们不使用异常,不是没有人应该使用它们。如果您查看基本原理,他们还会写:

由于Google现有的大多数C ++代码都不准备处理异常,因此采用产生异常的新代码相对困难。

通常的遗留问题。:-(


4
问题是他们如何处理该决定对STL容器接口的后果,而不是其他人怎么做;-)
Steve Jessop

4
不仅仅是遗留问题。编写异常安全代码并非易事,并且不存在(据我所知)可以帮助开发人员的工具。因此,即使是新法规也容易出现异常危险。
kirakun 2011年

2
问题不是“他们的决定是否合理,还是应该每个人都遵循他们的决定”。不,不,不。问题是,是否有可能使用stl容器并遵守此规则。
安德烈(Andrei)

3
公平地说。在他们反对例外的6个理由中,只有一个与遗产有关。其他五个“缺点”与遗产无关。
Andrei

12
它们的方法也有缺点,例如必须对对象使用所有的Init()函数。如果您忘记打个电话怎么办?如何处理临时对象?不使用那个吗?您是否为所有类都拥有一个空的Init()函数,或者只是在发现需要一个类时才添加一个?然后,如何找到所有需要添加Init()调用的地方?他们说例外迫使您使用RAII,这应该很难。如果不使用异常,则必须在整个位置检查返回码或对象有效性。这甚至更难解决。
Bo Persson

46

我们根本至少在应用程序级代码中,处理容器引发的异常。

自2008年以来,我一直是使用C ++的Google搜索工程师。我们确实经常使用STL容器。我个人无法回忆起曾经追溯到诸如vector :: push_back()或map :: operator []之类的单个重大故障或错误,在这里我们说:“噢,我们必须重写此代码,因为分配可能失败”或“当当,如果仅使用异常,则可以避免。” 进程是否耗尽了内存?是的,但这通常是一个简单的错误(例如,有人在程序中添加了一个大的新数据文件,却忘记增加RAM分配)或灾难性故障,无法恢复并继续进行操作。我们的系统已经自动管理并重新启动作业,以使磁盘,宇宙射线等出现故障的机器变得更强大,这确实没有什么不同。

据我所知,这里没有问题。


2
@Hinata Hyuga:感谢您的建议,但我同意Loki Astari;我的答案完全取决于我在Google上的个人经验,我认为这是回答Stack Overflow问题时的有效资料。我已经采纳了您的一些语法建议。谢谢,两个!
hoffmanj

就像我在下面的回答中说的那样,使用标准分配器时,在现代系统上无论如何都不会看到分配器异常。您可能会想到使用某种自定义分配器,但否则确实不值得担心。
9

2
@hoffmanj我已经看到了一些使用流的代码,其中基础流进入了失败状态,而代码却无声地失败了。每次插入后检查流的状态(ala WinAPI)将是令人讨厌的。当然,样式指南禁止将流用于非日志记录目的(顺便说一句,其中是否包括stringstream?)。我想我的问题是,如果您正在与STL构造进行交互,而STL构造除了异常之外没有很好的错误条件接口,您是否就直接禁止了它们?
布赖恩·范登堡

11

我敢肯定,他们的意思是他们不使用异常代码。如果您检查了他们的cpplint脚本,它会检查以确保您包括STL容器的正确标头(例如向量,列表等)。


1
嗯,他们是否将对stl方法的每个调用都包装到catch(...)中?
安德烈(Andrei)

3
标记,如果您的意思是“它们不会在代码中引发异常,但是它们允许调用的代码引发异常”,则此解释是错误的。单击规则左侧的小三角形。从说明中可以很明显地看出,他们也不希望底层代码抛出异常。
安德烈(Andrei)

4
如“规则的例外”部分所述,“在处理不符合此样式指南的代码时,您可能会偏离规则。” 当然,STL并非在设计时就考虑了Google约定。
伊斯梅尔

7

我发现Google明确提及了STL和异常(重点是我的):

尽管您不应该在自己的代码中使用异常,但是它们在ATL和某些STL(包括Visual C ++附带的STL)中得到了广泛使用。使用ATL时,应定义_ATL_NO_EXCEPTIONS以禁用异常。您应该调查是否还可以在STL中禁用异常,但是如果不能禁用,则可以在编译器中打开异常。(请注意,这仅是为了编译STL。您仍然不应该自己编写异常处理代码。

我不喜欢这样的决定(很幸运,我没有为Google工作),但是他们对自己的行为和意图很清楚。


7

无论如何,您都无法在现代操作系统上处理分配失败;作为性能优化,它们通常会过量使用内存。例如,如果您malloc()在Linux上调用并要求一个非常大的内存块,那么即使实际上不存在支持它所需的内存,它也会成功。只有当您访问它时,内核实际上才会尝试分配页面来支持它,这时告诉您分配仍然反而为时已晚。

所以:

  1. 除特殊情况外,不要担心分配失败。如果计算机内存不足,那将是灾难性的故障,您无法从中可靠地恢复。

  2. 但是,最好的方法是捕获未处理的异常并记录e.what()输出,然后重新记录throw,因为这可能比回溯提供更多信息,并且典型的C ++库实现不会自动为您执行此操作。

  3. 关于内存不足时如何不能依赖崩溃的上述整个主题很完整,而且很垃圾。C(++)标准可能无法保证,但是在现代系统上,如果内存不足,崩溃是您唯一可以依靠的东西。特别是,您不能依赖于NULL从分配器获得一个或什至其他任何指示,直到并包括一个C ++异常。

  4. 如果您发现自己在可访问页面零的嵌入式系统上,强烈建议您通过在该位置映射一个无法访问的页面来解决此问题。不能依靠人类在NULL任何地方检查指针,但是您可以通过一次映射页面来解决此问题,而不是尝试纠正某人可能错过了的所有可能的位置(过去,现在将来)NULL

我将通过说出以上条件来限定您使用的是某种自定义分配器,或者您所使用的系统没有过度使用(没有交换的嵌入式系统就是其中的一个例子,但并非唯一)例)。在这种情况下,也许可以在系统上优雅地处理内存不足的情况。但总的来说,在21世纪,恐怕您不太可能获得这个机会。首先,当事情开始崩溃时,您就会知道系统内存不足。


1
(1)运行OOM的机器与运行OOM的机器不同,至少对于仍然是32位的(大多数)进程而言:由于地址空间碎片,您将开始而不是后来遇到大块的分配失败。(正如我们在Windows上反复遇到的一样。)
Martin Ba

1
(2)如果您要崩溃(即核心转储)未处理的异常的过程,然后再分析崩溃转储,最好不要捕获异常(并重新抛出),因为捕获异常会解开堆栈,从而导致更多错误,或者至少混淆了皮疹转储。
马丁·巴

1
而且,纠正我,上一次我检查Windows并没有过多使用。这样就剩下了很大一部分安装基础,不是吗?
马丁·巴

@MartinBa听说您遇到地址空间碎片问题,感到很惊讶。对于大多数32位程序来说,这是一个不寻常的问题(那些最容易受到影响的程序在可能的情况下已经使用64位程序了)。
alastair 2013年

@MartinBa我不确定Windows是否不会过量使用。我记得在Win2K?上不小心分配了超过可能可用的内存,让它成功了,然后崩溃了。
alastair 2013年

4

Stl本身仅在内存分配失败的情况下直接抛出。但是通常,现实世界中的应用程序可能由于多种原因而失败,内存分配失败只是其中之一。在32位系统上,内存分配失败不是可以忽略的事情,因为它可能发生。因此,上面关于内存分配失败将不会发生的整个讨论是没有意义的。即使假设这样做,也必须使用两步初始化来编写一个代码。C ++异常处理在很长一段时间内就已经落后于64位体系结构。我不确定我应该在多大程度上尊敬google在这里显示的负面专业精神,只回答所提出的问题。我记得IBM在1997年前后发表的一篇论文,指出IBM的一些人对C ++异常处理的理解和理解程度。好的专业水平不一定是成功的指标。因此,如果使用STL,放弃异常处理不仅是一个问题。如果有人这样使用C ++,那就是一个问题。这意味着放弃

  • 构造函数失败
  • 能够将成员对象和基类对象用作以下任何基类/成员类构造函数的参数(无需任何测试)。难怪人们在C ++异常处理存在之前就使用了两步构建。
  • 在允许客户或第三方提供代码并抛出错误的环境中放弃分层和丰富的错误消息,而在编写调用代码时无法预见调用代码的原始编写者可能会提供的错误,并且可以提供空间在他的返回错误代码范围内。
  • 避免了诸如返回指向静态内存对象的指针以解决消息分配失败之类的骇客行为,FlexLm的作者所做的
  • 能够使用将地址返回到内存映射的稀疏文件中的内存分配器。在这种情况下,当一个人访问有问题的内存时,分配失败就会发生。(好吧,目前这仅在Windows上有效,但是苹果迫使gnu团队在G ++编译器中提供了必要的功能。此功能也适用于他们)(糟糕-这甚至适用于STL)
  • 能够将这种C样式的代码抛在后面(忽略返回值),并且必须使用带有调试可执行文件的调试器来找出在非琐碎的环境中(由第三方提供的子进程和共享库)失败的原因或进行远程执行
  • 能够将丰富的错误信息返回给调用者,而不仅仅是将所有内容都转储到stderr

3

在问题概述的假设下,只有一种可能性可以处理分配失败:

  • 分配器在分配失败时强制应用程序退出。特别是,这需要cusror分配器。

在这种情况下,越界索引异常就不那么有趣了,因为应用程序可以确保使用预检查不会发生这种情况。


1

尽管我没有看到Google的C ++异常处理与Go中的异常处理之间的任何比较,但在这里晚了。具体来说,Go仅通过内置error类型处理错误。链接的Golang博客帖子明确总结了

正确的错误处理是好的软件的基本要求。通过使用本文中描述的技术,您应该能够编写更可靠和简洁的Go代码。

Golang的创建当然考虑了使用C ++的最佳实践。所述底层的直觉是少可以成多。我没有在Google工作过,但确实发现他们使用C ++和创建Golang可能暗示着该公司的基本最佳实践。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.