正如其他人所指出的那样,这assert
是您防御永远不应该发生的程序员错误的最后堡垒。它们是健全性检查,希望在发货时不会左右失败。
出于开发人员可能会发现有用的任何原因:美观,性能以及他们想要的任何原因,它也被设计从稳定版本中删除。这是将调试版本与发布版本区分开的一部分,并且根据定义,发布版本没有此类断言。因此,如果您要发布类比的“有断言的发布版本”,则有一个设计的颠覆,这是尝试使用_DEBUG
预处理器定义但NDEBUG
未定义的发布版本。它实际上不再是发布版本。
该设计甚至扩展到标准库中。作为众多示例中的一个非常基本的示例,std::vector::operator[]
将执行assert
健全性检查的许多实现,以确保您不会对向量进行过界检查。如果在发布版本中启用了此类检查,则标准库将开始表现得差很多。基准vector
使用operator[]
并且在普通的老式动态数组中包含这样的断言的填充函数通常会显示动态数组要快得多,直到您禁用此类检查为止,因此它们通常确实以微不足道的方式影响性能。如果在代码之前的关键循环中,在关键帧的每帧中对这种检查进行数百万次的应用,则此处的空指针检查和在那里的边界检查实际上可能会成为一笔巨大的开销,就像取消引用智能指针或访问数组一样简单。
因此,您很可能需要用于该工作的另一种工具,并且如果要在关键区域执行这种健全性检查的发行版,则不应将其设计为在发行版中省略。我个人发现最有用的是日志记录。在那种情况下,当用户报告一个错误时,如果他们附加了一条日志,事情就会变得容易得多,并且日志的最后一行为我提供了一个有关错误发生在哪里以及可能是什么的线索。然后,在调试构建中重现他们的步骤时,我可能同样会遇到断言失败的问题,而断言失败会进一步给我提供大量线索来简化我的时间。但是,由于日志记录相对昂贵,因此我不使用它来进行极端低级的完整性检查,例如确保未在通用数据结构中超出限制地访问数组。
最后,在某种程度上与您达成一致,我可以看到一个合理的情况,您实际上可能希望在Alpha测试期间向测试人员提供类似于调试版本的东西,例如,与一小群Alpha测试人员签署了NDA 。如果您给测试人员提供的功能不是完整版本,而是附加了一些调试信息以及一些调试/开发功能(例如可以运行的测试以及运行软件时的详细输出),则可以简化Alpha测试。我至少已经看到一些大型游戏公司为alpha做类似的事情。但这是针对Alpha或内部测试之类的,您实际上是在尝试为测试人员提供发行版本以外的其他功能。如果您实际上是在尝试发布发行版本,那么按照定义,它应该没有_DEBUG
定义,否则确实会混淆“调试”和“发布”版本之间的差异。
为什么要在发布之前删除此代码?这些检查并不会减少性能,如果失败了,肯定会有一个问题,我希望有一个更直接的错误消息。
如上所述,从性能的角度来看,检查不一定是琐碎的。许多人可能是微不足道的,但即使标准库使用了它们,它也可能以许多人无法接受的方式影响性能,例如,std::vector
在经过优化的发行版中,如果遍历4倍的时间进行随机访问遍历,因为它的检查范围永远都不会失败。
在以前的团队中,我们实际上不得不使矩阵和向量库在某些关键路径中排除某些断言,只是为了使调试构建运行得更快,因为这些断言将数学运算的速度降低了一个数量级,直至达到原来的水平。开始要求我们等待15分钟,然后我们才可以找到感兴趣的代码。我的同事实际上只是想删除asserts
直截了当,因为他们发现这样做就产生了巨大的变化。相反,我们决定仅使关键的调试路径避免它们。当我们使这些关键路径不经边界检查而直接使用向量/矩阵数据时,执行完整操作(其中不仅仅包括向量/矩阵数学运算)所需的时间从几分钟减少到几秒钟。因此,这是一个极端的情况,但是从性能的角度来看,断言肯定并非总是可以忽略不计的,甚至是紧密的。
但这仅仅asserts
是设计的方式。如果它们对整体性能没有太大的影响,那么如果将它们设计为不仅仅是调试构建功能,或者如果vector::at
在发行版本中进行边界检查并超出范围,我们可能会喜欢它。的访问权限(例如,对性能的巨大影响)。但是,由于它们在我的案例中会对性能产生巨大影响,因此,我目前认为它们的设计有用得多,它是仅调试-构建功能,在NDEBUG
定义时将其省略。至少对于我曾经使用过的案例而言,它对于发行版本的排除非常重要,因为它排除了从不应该真正失败的健全性检查。
vector::at
与 vector::operator[]
我认为这两种方法的区别是替代方法:异常的核心。vector::operator[]
通常assert
,为了确保越界访问向量时要确保越界访问将触发易于重现的错误的实现。但是库实现者这样做的前提是,在优化的发行版本中它不会花费一毛钱。
同时vector::at
提供了始终执行越界检查并甚至在发行版本中都抛出异常的功能,但是它会降低性能,以至于我经常看到vector::operator[]
比使用更多的代码vector::at
。很多C ++的设计都呼应“为使用/需要的东西付费”的想法,并且很多人经常喜欢operator[]
,基于他们不这样做的想法,甚至不担心发行版本中的界限检查不需要在优化的发行版本中进行边界检查。突然之间,如果在发布版本中启用了断言,则这两者的性能将是相同的,并且向量的使用最终总是会比动态数组慢。因此,断言的设计和好处的很大一部分是基于这样的想法,即断言在发布版本中是免费的。
release_assert
在发现这些意图之后,这很有趣。当然,每个人的用例都会有所不同,但是我想我会为a release_assert
做些检查,并且即使在发行版本中,也会显示行号和错误消息的软件崩溃。
对于某些晦涩的情况,我不希望软件像抛出异常那样正常恢复。我希望即使在这种情况下,它也可以崩溃,以便可以给用户一个行号来报告软件何时遇到了永远不应该发生的事情,仍然处于对程序员错误而不是外部输入错误(例如:例外,但价格便宜,无需担心发布成本。
实际上,在某些情况下,我会发现行号和错误消息的严重崩溃比从抛出的异常中正常恢复更可取,而抛出异常可能很便宜,可以保留在版本中。在某些情况下,无法从异常中恢复,例如尝试从现有异常中恢复时遇到的错误。在那里,我找到了一个完美的选择,这release_assert(!"This should never, ever happen! The software failed to fail!");
自然很便宜,因为检查首先是在特殊路径中执行的,而在正常执行路径中不会花费任何费用。