尝试捕获或ifs在C ++中进行错误处理


30

是在游戏引擎设计中广泛使用了例外还是使用纯if语句更可取?例如,例外:

try {
    m_fpsTextId = m_statistics->createText( "FPS: 0", 16, 20, 20, 1.0f, 1.0f, 1.0f );
    m_cpuTextId = m_statistics->createText( "CPU: 0%", 16, 20, 40, 1.0f, 1.0f, 1.0f );
    m_frameTimeTextId = m_statistics->createText( "Frame time: 0", 20, 20, 60, 1.0f, 1.0f, 1.0f );
    m_mouseCoordTextId = m_statistics->createText( "Mouse: (0, 0)", 20, 20, 80, 1.0f, 1.0f, 1.0f );
    m_cameraPosTextId = m_statistics->createText( "Camera pos: (0, 0, 0)", 40, 20, 100, 1.0f, 1.0f, 1.0f );
    m_cameraRotTextId = m_statistics->createText( "Camera rot: (0, 0, 0)", 40, 20, 120, 1.0f, 1.0f, 1.0f );
} catch ... {
    // some info about error
}

和ifs:

m_fpsTextId = m_statistics->createText( "FPS: 0", 16, 20, 20, 1.0f, 1.0f, 1.0f );
if( m_fpsTextId == -1 ) {
    // show error
}
m_cpuTextId = m_statistics->createText( "CPU: 0%", 16, 20, 40, 1.0f, 1.0f, 1.0f );
if( m_cpuTextId == -1 ) {
    // show error
}
m_frameTimeTextId = m_statistics->createText( "Frame time: 0", 20, 20, 60, 1.0f, 1.0f, 1.0f );
if( m_frameTimeTextId == -1 ) {
    // show error
}
m_mouseCoordTextId = m_statistics->createText( "Mouse: (0, 0)", 20, 20, 80, 1.0f, 1.0f, 1.0f );
if( m_mouseCoordTextId == -1 ) {
    // show error
}
m_cameraPosTextId = m_statistics->createText( "Camera pos: (0, 0, 0)", 40, 20, 100, 1.0f, 1.0f, 1.0f );
if( m_cameraPosTextId == -1 ) {
    // show error
}
m_cameraRotTextId = m_statistics->createText( "Camera rot: (0, 0, 0)", 40, 20, 120, 1.0f, 1.0f, 1.0f );
if( m_cameraRotTextId == -1 ) {
    // show error
}

我听说异常比ifs慢一些,我不应该将异常与if-checking方法混合使用。但是,除了例外,在每个initialize()方法或类似方法之后,我的代码比起大量ifs更具可读性,尽管我认为对于某些方法而言,有时它们太笨重了。他们可以进行游戏开发还是更好地坚持简单的假设?


很多人声称Boost对游戏开发不利,但我建议您看看[“ Boost.optional”](boost.org/doc/libs/1_52_0/libs/optional/doc/html/index.html)您的ifs示例
要好

Andrei Alexandrescu 很好地谈论了错误处理,我对他使用了类似的方法,没有例外。
Thelvyn

Answers:


48

简短答案:请阅读Niklas Frykholm的“ 敏感错误处理1”,“ 敏感错误处理2 ”和“ 敏感错误处理3”。实际上,在您浏览博客时,请阅读该博客上的所有文章。不会说我同意所有事情,但其中大多数是黄金。

不要使用例外。有很多原因。我将列出主要的。

它们确实可以变慢,尽管在较新的编译器上已将其最小化了。一些编译器为实际上不触发异常的代码路径支持“零开销异常”(尽管这有点荒谬,因为异常处理仍需要额外的数据,从而使可执行文件/ dll的大小变大)。但是最终结果是,是的,使用异常的速度较慢,并且在任何对性能至关重要的代码路径中,您都应绝对避免使用它们。根据您的编译器,完全启用它们可能会增加开销。它们始终总是膨胀代码大小,在许多情况下相当大,这会严重影响当今硬件的性能。

例外使代码很多更加脆弱。有一个臭名昭著的图形(我现在很难找到它),基本上只是在图表上显示了编写异常安全代码与异常非安全代码的难度,而前者是一个很大的栏。只是有很多带有异常的小陷阱,而且有许多种写代码的方法看起来很安全,但实际上并非如此。甚至整个C ++ 11委员会都对此感到厌烦,并且忘记为正确使用std :: unique_ptr添加重要的帮助器函数,即使有了这些帮助器函数,使用它们也需要更多的键入操作,大多数程序员都不会这样做。甚至没有意识到如果不这样做怎么了。

更具体地说,对于游戏行业,某些游戏机的供应商提供的编译器/运行时完全不支持例外,甚至根本不支持例外。如果您的代码使用异常,也许您仍然处于当今时代,必须重写部分代码才能将其移植到新平台上。(不确定自从上述控制台发布以来的7年里这种情况是否有所改变;我们只是不使用异常,甚至在编译器设置中将其禁用,所以我不知道与我交谈的任何人最近都进行了检查。)

总体思路很明确:在特殊情况下使用例外。当您的程序进入“我不知道该怎么办,也许希望其他人去做,所以我会抛出异常,看看会发生什么”时,请使用它们。如果没有其他选择有意义,请使用它们。如果您不小心泄漏了一点内存或由于清理了正确的智能句柄而无法清理资源,则可以使用它们。在所有其他情况下,请勿使用它们。

对于像您的示例这样的代码,您还有其他几种方法可以解决该问题。尽管在您的简单示例中不一定是最理想的方法,但是更可靠的方法之一是倾向于使用monadic错误类型。也就是说,createText()可以返回自定义句柄类型,而不是整数。此句柄类型具有用于更新或控制文本的访问器。如果将句柄置于错误状态(因为createText()失败),则对该句柄的进一步调用只会静默失败。您还可以查询该句柄以查看其是否有错误,如果有,则最后一次错误是什么。这种方法比其他方法具有更多的开销,但它相当可靠。如果需要在某些情况下执行一长串操作,而任何情况下任何单个操作都可能导致生产失败,但是您却无法/不能/成功了,请使用它

实现单子错误处理的一种替代方法是使上下文对象上的方法优雅地处理无效的句柄ID,而不是使用自定义句柄对象。例如,如果createText()失败时返回-1,则如果传入-1,则对采用这些句柄之一的m_statistics的任何其他调用应正常退出。

您也可以将错误打印放入实际上失败的函数中。在您的示例中,createText()可能会提供有关发生问题的更多信息,因此它可以将更有意义的错误转储到日志中。在这种情况下,将错误处理/打印输出给调用者没有什么好处。当调用者需要自定义处理(或使用依赖项注入)时,执行此操作。请注意,拥有一个可以在每次记录错误时弹出的游戏机控制台是一个好主意,并且在这里也有所帮助。

对于在任何理智的环境中都不会失败的呼叫,最好的选择(已经在上面的链接的系列文章中已经介绍过)(例如,为统计系统创建文本Blob的简单动作)就是拥有该功能失败(在您的示例中为createText)中止。您可以合理地确定createText()不会在生产中失败,除非完全消失了(例如,用户删除了字体数据文件,或者由于某种原因仅具有256MB的内存等)。在许多情况下,当确实发生故障时,甚至没有什么好做的。记不清?您甚至可能无法进行必要的分配,才能创建一个不错的GUI面板向用户显示OOM错误。缺少字体?使得难以向用户显示错误。出什么事了

只要您(a)将错误记录到日志文件中,以及(b)仅针对不是由常规用户操作引起的错误,就执行崩溃操作,就完全可以了。

对于可用性至关重要且监视程序监视还不够的许多服务器应用程序,我根本不会说同一件事,但这与游戏客户端开发完全不同。我也强烈建议您不要在这里使用C / C ++,因为其他语言的异常处理工具往往不像C ++那样咬人,因为它们是托管环境,并且没有C ++所具有的所有异常安全问题。由于服务器趋向于更多地关注并行性和吞吐量,而不是像游戏客户端这样的最小延迟保证,因此任何性能问题都得到了缓解。例如,即使是射击游戏之类的动作游戏服务器,也可以使用C#编写,因此运行良好,因为它们很少像FPS客户端那样将硬件推到极限。


1
+1。另外,有可能warn_unused_result在某些编译器中添加类似于功能的属性,从而允许捕获未处理的错误代码。
Maciej Piechotka

来到这里看到标题中的C ++,并且完全确定单子错误处理将不会在这里。好东西!
亚当

在性能不是很关键并且您主要目标是快速实施的地方呢?例如,当您实现游戏逻辑时?
Ali1S232

将异常处理仅用于调试目的的想法又如何呢?就像发布游戏时一样,您希望一切顺利进行而不会出现问题。因此,您可以在开发过程中使用异常查找和修复错误,然后在发布模式下删除所有这些异常。
Ali1S232

1
@Gajoo:对于游戏逻辑,异常只会使逻辑更难遵循(因为这会使所有代码难以遵循)。根据我的经验,即使在Pythnn和C#中,游戏逻辑也很少出现异常。对于调试,硬断言通常更方便。在出现问题的确切时刻中断并停止,而不是在展开堆栈的大部分并丢失由于异常抛出语义而丢失的各种上下文信息之后。对于调试逻辑,您需要构建工具和信息/编辑GUI,以便设计人员可以检查和调整。
肖恩·米德迪奇

13

唐纳德·克努斯本人的古老智慧是:

“我们应该忘记效率低下的问题,大约有97%的时间是这样:过早的优化是万恶之源。”

将其打印为大海报,并将其挂在认真进行编程的任何地方。

因此,即使如果 try / catch语句是慢一点,那么我会在默认情况下使用它:

  • 该代码必须尽可能地易于理解。您可能只编写一次代码,但是您阅读更多次以调试该代码,调试其他代码,理解,改进...

  • 性能上的正确性。正确地进行if / else东西并非易事。在您的示例中,此操作未正确完成,因为不能显示一个错误,而是可以显示多个错误。您必须使用级联if / then / else。

  • 始终易于使用:Try / catch包含不需要检查每一行错误的样式。在另一方面:一个失踪的if / else和你的代码可能造成严重破坏。当然只有在不可复制的情况下。

所以最后一点:

我听说例外情况比ifs慢一些

我听说大约15年前,最近还没有从任何可靠的消息来源听到此消息。编译器可能有所改进或其他。

要点是:不要过早优化。做到这一点时,才可以通过基准,手头的代码是一些频繁使用的代码紧凑的内部循环证明,并在切换风格提高了性能相当。这是3%的情况。


22
我将-1设为无穷大。我无法理解Knuth引用对开发人员的大脑造成的伤害。考虑是否使用异常不是过早的优化,这是一个设计决策,这是一个重要的设计决策,其后果远不止性能。
sam hocevar

3
@SamHocevar:对不起,我不明白你的意思:问题关于使用异常时可能的性能下降。我的观点:不要考虑,命中率还不错(如果有的话),而其他东西则更为重要。在这里,您似乎同意在“我的清单”中添加“设计决策”。好。另一方面,您说Knuth的报价很不好,暗示过早的优化还不错。但是,这正是IMO发生的情况:问:Q不考虑体系结构,设计或其他算法,仅考虑异常及其性能影响。
AH

2
我不同意C ++中的清晰性。C ++具有未检查的异常,而某些编译器实现了检查的返回值。结果,您的代码看起来更清晰,但可能隐藏了内存泄漏,甚至使代码处于未定义状态。同样,第三方代码可能也不是例外安全的。(Java / C#中的情况有所不同,它们已经检查了异常,GC等)。从设计决策开始-如果他们不交叉API点,则可以使用perl一次性代码半自动地完成从/到每种样式的重构。
Maciej Piechotka

1
@SamHocevar:“ 问题是关于什么是更可取的,而不是更快的是什么。 ”重新阅读问题的最后一段。他甚至考虑不使用异常的唯一原因是,他认为异常处理速度可能较慢。现在,如果您认为OP没有考虑到其他问题,请随时发布或赞扬那些关注的人。但是OP非常明确地专注于异常的执行。
Nicol Bolas

3
@AH-如果您要引用Knuth,请不要忘记其余部分(也是IMO最重要的部分):“ 但是,我们不应该放弃这3%的关键机会。优秀的程序员不会通过这种推理使自己感到沾沾自喜,明智的做法是,他仔细查看关键代码;但是只有在确定了该代码之后, ” 人们常常把这当作根本不优化的借口,或者在性能不是优化但实际上是基本要求的情况下尝试证明代码缓慢。
Maximus Minimus

10

这个问题已经有了一个很好的答案,但是我想对您的特殊情况添加一些有关代码可读性和错误恢复的想法。

我相信您的代码实际上可能是这样的:

m_fpsTextId = m_statistics->createText( "FPS: 0", 16, 20, 20, 1.0f, 1.0f, 1.0f );
m_cpuTextId = m_statistics->createText( "CPU: 0%", 16, 20, 40, 1.0f, 1.0f, 1.0f );
m_frameTimeTextId = m_statistics->createText( "Frame time: 0", 20, 20, 60, 1.0f, 1.0f, 1.0f );
m_mouseCoordTextId = m_statistics->createText( "Mouse: (0, 0)", 20, 20, 80, 1.0f, 1.0f, 1.0f );
m_cameraPosTextId = m_statistics->createText( "Camera pos: (0, 0, 0)", 40, 20, 100, 1.0f, 1.0f, 1.0f );
m_cameraRotTextId = m_statistics->createText( "Camera rot: (0, 0, 0)", 40, 20, 120, 1.0f, 1.0f, 1.0f );

没有例外,没有假设。错误报告可以在中完成createText。并createText可以返回不需要您检查返回值的默认纹理ID,以便其余代码也可以正常工作。

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.