您发现C ++代码中的异常安全性有多重要?


19

每当我考虑使我的代码强烈地异常安全时,我就不这样做,因为这样做会非常耗时。考虑以下相对简单的代码段:

Level::Entity* entity = new Level::Entity();
entity->id = GetNextId();
entity->AddComponent(new Component::Position(x, y));
entity->AddComponent(new Component::Movement());
entity->AddComponent(new Component::Render());
allEntities.push_back(entity); // std::vector
entityById[entity->id] = entity; // std::map
return entity;

为了实现基本的异常保证,我可以在new调用中使用作用域指针。如果任何调用引发异常,这将防止内存泄漏。

但是,假设我要实现一个强大的异常保证。至少,我需要实现我的容器共享光标(我不使用升压),一个无抛出Entity::Swap的原子添加组件和某种成语的原子添加到两个VectorMap。实施这些不仅耗时,而且由于与异常不安全解决方案相比涉及更多的复制,因此它们也很昂贵。

最终,在我看来,花时间做所有这些事情是不合理的,因此一个简单的CreateEntity函数是绝对例外安全的。我可能只想让游戏显示一个错误并在该点关闭。

您在自己的游戏项目中走了多远?为可能会在出现异常时崩溃的程序编写异常不安全代码,通常是否可以接受?


21
过去,当我从事C ++项目时,基本上我们假装不存在异常。
Tetrad 2012年

2
我个人非常喜欢异常,并且经常喜欢弄清楚如何使我的代码提供有力的保证。如果您在遇到异常时崩溃了……也许不要理会它。

7
我个人使用异常来管理.. hm ..'exceptions'不是错误。就像,如果我知道某个函数可能崩溃,那么我会让它崩溃,但是如果我知道某个函数可能无法成功读取档案,但是还有另一种方法,我可以尝试捕获它。如果它是“无法读取”的异常,我将以另一种方式来管理它,而无需归档读取。
Gustavo Maciel

Gtoknu所说的,您确实需要知道任何外部库可能抛出的异常。
彼得Ølsted,2012年

Answers:


26

如果要使用异常(并不是说应该使用异常,则该方法的优缺点不在此问题的特定范围之内),则应正确编写异常安全代码。

不使用异常并且明确地不具有异常安全性的代码比使用它们的代码好,但是它对异常安全性的评价却是一半。如果是后一种情况,那么当发生异常而您可能根本无法恢复的异常时,可能会发生一整类错误和故障,从而使异常的好处之一完全无效。即使是相对罕见的故障类别,它仍然是可能的。

这并不是说,如果您使用异常,则所有代码都必须提供强大的异常保证-只是每段代码都应提供某种类型的保证(无,基本,强大),以便在使用该代码时您知道消费者可以提供的保证。通常,所讨论的组件的级别越低,保证应越强。

如果您永远不会提供有力的保证或永远不会真正接受异常处理范例以及所隐含的所有其他工作和缺点,那么您也就不会真正获得它所隐含的所有优点,并且可能会更轻松时间完全放弃了例外。


12
仅此一项就值得+1:“不使用异常并且明确地不具有异常安全性的代码要比使用它们但却将其异常安全性提高一半的代码更好。”
Maximus Minimus

14

我的一般经验法则:如果需要使用例外,请使用例外。如果您不需要使用例外,请不要使用例外。如果您不知道是否需要使用例外,则不需要使用例外。

在始终在线的关键任务应用程序中,绝对没有您的最后一道防线。如果您要编写绝对不会失败的空中交通管制软件,请使用异常。如果要编写核电站的控制代码,请使用异常。

另一方面,在开发游戏时,遵循这一口头禅要好得多:尽早崩溃,大声崩溃。如果有问题,您真的想尽快知道。您永远都不希望错误地“处理”错误,因为这通常会掩盖以后发生不当行为的实际原因,并使调试比原本要困难得多。


5

例外的问题是,无论如何,您都必须处理它们。如果由于内存不足而导致异常,则可能仍然无法处理该错误。也可以将整个游戏转储到一个巨大的异常处理程序中,该异常处理程序会弹出错误框。

对于游戏开发,我更喜欢尝试并找到方法使我的代码在可能的情况下继续失败。

例如,我将一个特殊的ERROR网格硬编码到我的游戏中。这是一个旋转的红色圆圈,上面带有白色X和白色边框。根据游戏的不同,一个看不见的游戏可能会更好。它具有一个在启动时初始化的单例实例。由于它被烘焙到代码中,而不是使用任何外部文件,所以它应该不会失败。

如果正常的loadMesh调用失败,而不是抛出一些错误并崩溃到桌面,它将以静默方式将失败记录到控制台/日志文件中,并返回硬编码错误网格。我发现,当mod搞砸事情时,Skyrim实际上会做同样的事情。玩家很有可能甚至看不到错误网格(除非存在一些重大问题,并且其中许多问题都与此类似)。

还有一个后备着色器。它执行基本的顶点矩阵运算,但是如果由于某种原因没有统一的矩阵,则仍应执行正交视图。它没有适当的阴影/照明/材质(尽管它确实具有color_overide标志,所以我可以将其与错误网格一起使用)。

唯一可能的问题是错误网格物体版本是否可能以某种方式永久破坏游戏。例如,确保它不应用物理,因为如果玩家加载一个区域,则错误网格可能会落在地板上,然后如果他们这次以正确的网格工作重新加载游戏,则可能更大。埋在地下。这应该不太困难,因为您可能要存储物理网格和图形网格。脚本编写也可能是一个问题。有了一些东西,您将不得不死。不太确定后退“级别”的用途是什么(尽管您可以在一个小房间里贴上一个标有错误的标语,但请确保已禁用保存功能,因为您不想让玩家陷入其中)。

在“新”游戏中,对于游戏开发而言,最好使用某种工厂。它可以为您提供其他各种优势,例如在实际需要它们之前预先分配对象和/或将它们批量制造。它还使创建可用于内存管理的对象的全局索引之类的事情变得更容易,通过id查找事物(尽管您也可以在构造函数中进行此操作)。当然,您也可以在那里处理分配错误。


2

缺少运行时检查和崩溃的最大问题是,黑客有可能使用崩溃来接管进程,甚至可能接管机器。

现在,黑客无法利用实际的崩溃,一旦崩溃发生,该过程将变得无用且无害。如果您只是从空指针中读取数据,那将是完全崩溃,那么您将犯错,并且该过程将立即终止。

当执行的命令在技术上不合法但不是您打算执行的命令时,危险就来了,那么黑客可能能够使用经过精心设计的数据来指挥该操作,否则这些数据应该是安全的。

未捕获的异常实际上并不是真正的崩溃,它只是终止程序的一种方法。发生异常仅是因为有人编写了在特定情况下专门引发异常的库。通常是为了避免进入意料之外的情况,这可能是黑客的机会。

实际上,危险的举动是捕获异常而不是让异常滑行,这并不是说您永远都不要这样做,而是要确保只捕获那些您知道可以处理的异常并让其余的滑行。否则,您将冒着撤消小心地放入磁带库的安全措施的风险。

将重点放在不一定引发异常的错误上,例如在数组末尾写。您的程序不能偶然做到吗?


2

您需要查看您的异常,并查看可能触发它们的条件。在很多时间里,通过在开发/调试构建/测试周期中进行适当的检查可以避免人们使用异常的很大一部分。使用断言,传递引用,始终在声明时初始化局部变量,将所有分配的内存集设为零,释放后为NULL等,并且很多理论上的异常情况都将消失。

考虑:如果某件事失败并且可能引发异常,这会产生什么影响?有多少关系?如果您在游戏中的内存分配失败(以您的原始示例为例),那么您手头通常会遇到比处理失败案例更大的问题,并且通过异常处理失败案例不会带来更大的问题走开。更糟的是,它实际上可能掩盖了更大的问题甚至存在的事实。你想要那个吗?我知道我不知道 我知道,如果我无法通过内存分配,我想崩溃并进行可怕的刻录,并尽可能地接近故障点,这样我就可以闯入调试器,弄清楚发生了什么,并正确地进行修复。


1

不知道在SE上引用其他人是否合适,但我认为没有其他答案真正涉及到此。

引用Jason Gregory的著作《Game Engine Architecture》。(好书!)

“结构化异常处理(SEH)给程序增加了很多开销。每个堆栈帧都必须增加以包含堆栈展开过程所需的其他信息。而且,堆栈展开通常很慢-大约为2到3比简单地从函数返回要昂贵得多,而且,即使程序(或库)中的一个函数都使用SEH,您的整个程序也必须使用SEH。你抛出一个例外。”

话虽如此,我正在构建的引擎没有使用异常。这是因为前面提到的潜在性能成本,并且出于我的所有目的,错误返回代码也可以正常工作。我真正需要寻找失败的唯一时间是初始化和加载资源,在这种情况下,返回一个布尔值表示成功就可以了。


2
结构化异常处理是Windows上可用的一种特殊类型的异常处理,通常与C ++异常不同。
Kylotan

h,我简直不敢相信。感谢您的指正!
KlashnikovKid 2012年

0

我总是使我的代码异常安全,仅仅是因为它使人们更容易知道问题出在哪里,因为无论何时生成或捕获异常,我都可以记录一些东西。

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.