【问题标题】:Google C++ style guide's No-exceptions rule; STL?Google C++ 风格指南的无例外规则; STL?
【发布时间】:2011-07-08 05:36:34
【问题描述】:

Google 的 C++ style guide 表示“我们不使用例外”。该风格没有提到关于异常使用的 STL。由于 STL 分配器可能会失败,它们如何处理容器抛出的异常?

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

【问题讨论】:

  • 在我进来大喊大叫之前,我工作的商店也有同样愚蠢的惯例。我们只是忽略了现实。我打赌这也是谷歌所做的。
  • Google 有相当古老的标准。猜猜他们雇佣了很多学生,只能负担得起少数 C++ 向导。
  • @Maxim:这就是为什么您需要断言空指针,以遵循该建议。如果您不断言,则您的代码可能无法崩溃和烧毁,因为“未定义的行为”确实不是意味着“立即发生段错误”。诚然,风险很小,但请考虑例如theregister.co.uk/2009/07/17/linux_kernel_exploit
  • @Maxim:“在 0 地址映射页面是自找麻烦。” - 相当,攻击者通常是在恶意拉一些这样的特技时自找麻烦。不过,这只是一个例子。如果您表现得好像保证访问空指针会出现段错误,那么最终编译器会因不保证而使您(或像您这样的其他人)感到惊讶。程序启动后,未定义的行为可以及时返回,让恶魔飞出你的鼻子。要么尽快失败,要么不失败,但不要期望失败,然后就不会失败。

标签: c++ exception stl


【解决方案1】:

他们说他们不使用异常,并不是说没有人应该使用它们。如果你看看他们也写的理由:

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

常见的遗留问题。 :-(

【讨论】:

  • 问题是他们如何处理该决定对 STL 容器接口的影响,而不是其他人如何处理 ;-)
  • 不仅仅是遗留问题。编写异常安全代码并非易事,也没有工具(据我所知)可以帮助这里的开发人员。因此,即使是新代码也容易出现异常危险。
  • 问题不是“他们的决定是否合理,或者每个人都应该遵循他们的决定”。不,不,也不。问题是,是否可以使用 stl 容器并遵守此规则。
  • 公平地说。在他们反对例外的 6 个理由中,只有一个与遗产有关。其他五个“缺点”与传统无关。
  • 他们的方法也有缺点,比如对象的所有 Init() 函数。如果你忘记了一个电话怎么办?您如何处理临时对象?不要用那个?您是否对所有类都有一个空的 Init() 函数,或者当您发现需要一个时才添加一个?那么如何找到所有需要添加 Init() 调用的地方呢?他们说异常会迫使您使用 RAII,这应该很难。如果不使用异常,则必须到处检查返回码或对象有效性。这更难做到。
【解决方案2】:

我们只是处理容器抛出的异常,至少在应用程序级代码中是这样。

自 2008 年以来,我一直是 Google 搜索的工程师,使用 C++ 工作。我们确实经常使用 STL 容器。我个人无法回忆起一个重大故障或错误,它曾被追溯到诸如 vector::push_back() 或 map::operator[] 失败之类的东西,我们说“哦,伙计,我们必须重写这段代码,因为分配可能失败”或“该死,如果我们只使用异常,就可以避免这种情况。”进程是否会耗尽内存?是的,但这通常是一个简单的错误(例如,有人向程序添加了一个大的新数据文件并忘记增加 RAM 分配)或没有好的方法来恢复和继续的灾难性故障。我们的系统已经自动管理和重新启动作业,以便对具有故障磁盘、宇宙射线等的机器具有鲁棒性,这真的没有什么不同。

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

【讨论】:

  • @Hinata Hyuga:感谢您的建议,但我同意 Loki Astari;我的回答完全依赖于我在 Google 的个人经历,我相信这是回答 Stack Overflow 问题时的有效来源。不过,我已经采纳了您对语法的一些建议。谢谢,两位!
  • 正如我在下面的回答中所说,在使用标准分配器时,您不太可能在现代系统上看到分配器异常。您可能会使用某种自定义分配器,但除此之外真的不值得担心。
  • @hoffmanj 我已经看到一些使用流的代码,其中底层流进入失败状态并且代码静默失败。每次插入后检查流的状态(ala WinAPI)是令人讨厌的。当然,风格指南禁止将流用于非日志记录目的(顺便说一句,这是否包括字符串流?)。我想我的问题是,如果您正在与一个 STL 构造进行交互,该构造除了异常之外没有很好的错误条件接口,您会直接禁止它们吗?
【解决方案3】:

我很确定他们的意思是他们不在他们的代码中使用异常。如果您查看他们的cpplint script,它会检查以确保您包含正确的 STL 容器标头(如矢量、列表等)。

【讨论】:

  • Homm 那么他们是否将对 stl 方法的每个调用包装到 catch(...) 中?
  • 马克,如果你的意思是“他们不会在代码中抛出异常,但他们允许他们调用的代码抛出异常”,这种解释是错误的。单击规则左侧的小三角形。从解释中可以清楚地看出,他们也不希望底层代码抛出异常。
  • 如“规则的例外”部分所述,“在处理不符合本样式指南的代码时,您可能会偏离规则。”当然,STL 在设计时并未考虑到 Google 约定。
【解决方案4】:

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

虽然你不应该在你自己的代码中使用异常,但是它们被使用了 广泛存在于 ATL 和一些 STL 中,包括 使用 Visual C++。使用 ATL 时,您应该定义 _ATL_NO_EXCEPTIONS 禁用异常。你应该调查是否 你也可以在你的 STL 中禁用异常,但如果没有,它是 确定在编译器中打开异常。 (请注意,这只是为了 让 STL 编译。 你还是不要写异常处理 自己编码。

我不喜欢这样的决定(幸好我不是为 Google 工作),但他们非常清楚自己的行为和意图。

【讨论】:

    【解决方案5】:

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

    所以:

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

    2. 尽管如此,捕获未处理的异常并记录 e.what() 输出,然后重新输入throw 是一种很好的做法,因为这可能比回溯提供更多信息,而典型的 C++ 库实现不这样做自动为您服务。

    3. 上面关于内存不足时如何不能依赖崩溃的整个大线程是完整的,完全是垃圾。 C(++) 标准可能无法保证这一点,但在现代系统崩溃是唯一如果内存不足时您可以依靠的事情。特别是,您不能依赖从分配器获得NULL 或任何其他指示,直到并包含 C++ 异常。

    4. 如果您发现自己在一个可访问页面零的嵌入式系统上,我强烈建议您通过在该位置映射一个不可访问的页面来解决此问题。不能依赖人类到处检查NULL 指针,但是您可以通过映射页面一次 来解决这个问题,而不是尝试纠正所有可能的(过去、现在 未来)可能会错过NULL的位置。

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

    【讨论】:

    • (1)运行OOM的机器和运行OOM的进程不同,至少对于(大部分)仍然是32bit的进程:由于地址空间碎片,你会开始命中分配失败对于大块,宜早不宜迟。 (正如我们在 Windows 上反复体验的那样。)
    • (2) 如果你想崩溃(即核心转储)未处理异常的进程并稍后分析崩溃转储,最好捕获异常(并重新抛出)因为捕获它们会展开堆栈,从而导致更多错误或至少混淆 rash dump。
    • 而且,纠正我,上次我检查时,Windows 没有过度使用。所以这就留下了相当大一部分的安装基础需要考虑,不是吗?
    • @MartinBa 很惊讶听到您有地址空间碎片问题。对于大多数 32 位程序来说,这是一个非常不寻常的问题(那些最容易受到影响的程序可能已经使用 64 位)。
    • @MartinBa 我不太确定 Windows 不会过度使用。我记得,在 Win2K 上,不小心分配了比可能可用的更多的内存,并让它成功然后崩溃。
    【解决方案6】:

    Stl 本身只是在内存分配失败的情况下直接抛出。但通常现实世界的应用程序可能会因多种原因而失败,内存分配失败只是其中之一。在 32 位系统上,内存分配失败不应被忽略,因为它可能会发生。所以上面关于内存分配失败不会发生的整个讨论是毫无意义的。即使假设这一点,也必须使用两步初始化来编写代码。并且 C++ 异常处理比 64 位架构早了很长时间。 我不确定我应该在多大程度上尊重谷歌在这里展示的负面专业精神,只回答所提出的问题。我记得 1997 年左右 IBM 的一篇论文指出 IBM 的一些人对 C++ 异常处理的含义的理解和欣赏程度如何。好的 专业性不是必需的 一个成功的指标。 因此,如果使用 STL,放弃异常处理不仅是一个问题。如果这样使用 C++,那将是一个问题。这意味着放弃

    • 构造函数失败
    • 能够将成员对象和基类对象用作以下任何基类/成员类构造函数的参数(无需任何测试)。难怪人们在 C++ 异常处理出现之前就使用了两步构造。
    • 在允许客户或第三方提供代码并引发错误的环境中放弃分层和丰富的错误消息,调用代码的原始编写者在编写调用代码时不可能预见到这些错误,并且可以在他的返回错误代码范围内提供了空间。
    • 避免像 FlexLm 的作者所做的那样,将指向静态内存对象的指针返回到消息分配失败等黑客行为
    • 能够使用内存分配器将地址返回到内存映射的稀疏文件中。在这种情况下,当访问有问题的内存时会发生分配失败。(好吧,目前这只适用于 Windows,但苹果强迫 gnu 团队在 G++ 编译器中提供必要的功能。来自 Linux g++ 开发人员的更多压力将需要提供这个功能也适用于他们)(哎呀——这甚至适用于 STL)
    • 能够将这种 C 风格的编码抛在脑后(忽略返回值),并且必须使用带有调试可执行文件的调试器来找出在具有第三方提供的子进程和共享库的非平凡环境中发生的故障或正在做的事情远程执行
    • 能够将丰富的错误信息返回给调用者,而无需将所有内容都转储到 stderr

    【讨论】:

      【解决方案7】:

      根据问题中概述的假设,只有一种处理分配失败的可能性:

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

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

      【讨论】:

        【解决方案8】:

        虽然我没有看到 Google 的 C++ 异常处理和 Go 中的异常处理之间存在任何比较,但我迟到了。具体来说,Go only has error handling 通过内置的 error 类型。链接的 Golang 博客文章明确结束

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

        Golang 的创建当然考虑了使用 C++ 的最佳实践。 underlying intuition is that less can be more。我没有在 Google 工作过,但确实发现他们对 C++ 的使用和 Golang 的创建可能暗示了公司的基本最佳实践。

        【讨论】:

          猜你喜欢
          • 2023-03-22
          • 2014-07-04
          • 1970-01-01
          • 2019-08-24
          • 2022-01-08
          • 1970-01-01
          • 1970-01-01
          • 2017-11-26
          • 2011-01-13
          相关资源
          最近更新 更多