发布版本中应该有断言


20

assertC ++中的默认行为是在发行版本中不执行任何操作。我认为这样做是出于性能原因,也许是为了防止用户看到讨厌的错误消息。

但是,我认为,那些assert将被触发但被禁用的情况更加麻烦,因为应用程序可能因为某些不变式被破坏而以更糟糕的方式崩溃。

另外,对我而言,性能参数仅在可衡量的问题时才有效。assert我代码中的大多数不会比复杂得多

assert(ptr != nullptr);

这将对大多数代码产生很小的影响。

这使我想到一个问题:断言(意味着概念,而不是特定的实现)是否应该在发布版本中有效?为什么不)?

请注意,此问题不是关于如何在发布版本中启用断言(例如#undef _NDEBUG或使用自定义断言实现)。此外,这不是在第三方/标准库代码中启用断言,而是在我控制的代码中启用断言。


我知道医疗保健市场的全球参与者在客户现场安装了医院信息系统的调试版本。他们与Microsoft达成了一项特殊协议,也要在那里安装C ++调试库。好吧,他们的产品质量是……
Bernhard Hiller '18

2
一句古老的谚语说:“删除断言就像穿救生衣在海港练习,但是当您的船驶向公海时将救生衣抛在后面”wiki.c2.com/?AssertionsAsDefensiveProgramming) 。我个人默认在发布版本中将其启用。不幸的是,这种做法在C ++世界中不是很普遍,但是至少著名的C ++老手James Kanze总是争论主张在断言版本中保留断言:stackoverflow.com/a/12162195/3313064
Christian Hackl

Answers:


20

经典 assert是旧标准C库中的工具,而不是C ++中的工具。至少出于向后兼容的原因,它仍在C ++中可用。

我没有C标准库的确切时间表,但是我很确定assertK&R C投入使用后不久(1978年左右)就可以使用C标准库。在经典C语言中,要编写健壮的程序,需要比在C ++中更频繁地添加NULL指针测试和数组边界检查。可以通过使用引用和/或智能指针而不是指针来避免NULL指针测试的麻烦,并且通过使用std::vector,数组边界检查通常是不必要的。而且,1980年的表现肯定比今天重要得多。因此,我认为这很可能是因为默认情况下将“声明”设计为仅在调试版本中处于活动状态的原因。

此外,对于生产代码中的实际错误处理,在大多数情况下,仅测试某些条件或不变量并在不满足条件的情况下使程序崩溃的功能就不够灵活。对于调试可能没问题,因为运行程序并观察错误的人员通常会准备好调试器来分析发生的情况。但是,对于生产代码,明智的解决方案需要是一种功能或机制,

  • 测试某些条件(并在条件失败的范围内停止执行)

  • 在条件不成立的情况下提供清晰的错误消息

  • 允许外部示波器获取错误消息并将其输出到特定的通信通道。该通道可能类似于stderr,标准日志文件,GUI程序中的消息框,常规错误处理回调,启用网络的错误通道或最适合特定软件的通道。

  • 允许外部作用域根据情况决定程序是否应该正常结束或是否应该继续。

(当然,在某些情况下,如果情况未满足,则立即结束程序是唯一明智的选择,但是在这种情况下,它也应该在发行版本中发生,而不仅仅是调试版本中)。

由于经典版本assert不提供此功能,因此假设发布版本是部署到生产环境中的版本,则它就不太适合发布版本。

现在,您可以问为什么在C标准库中没有提供这种灵活性的这种功能或机制。实际上,在C ++中,有一种具有所有这些功能(以及更多功能)的标准机制,而且您知道这一点:它称为exceptions

但是,在C语言中,由于缺少作为编程语言一部分的异常,因此很难使用上述所有功能实现良好的通用标准错误处理标准机制。因此,大多数C程序都有自己的错误处理机制,这些机制带有返回码或“ goto”或“ long jumps”或两者的混合。这些通常是实用的解决方案,适合特定类型的程序,但不够“通用”,无法适合C标准库。


啊,完全忘记了C的遗产(毕竟#include <cassert>)。现在完全有道理了。
没人

1
我完全不同意整个答案。当您的应用程序发现其源代码有错误时,引发异常是在Java之类的语言中做不到的最后选择,Java不能做得更好,但是在C ++中,在这种情况下一定要立即终止程序。您不希望堆栈退卷导致此时进一步执行任何操作。一个有漏洞的程序可能会将可能损坏的数据写入文件,数据库或网络套接字。尽快崩溃(并且可能通过外部机制重新启动进程)。
Christian Hackl

1
@ChristianHackl:我同意在某些情况下,终止程序是唯一可行的选择,但随后它也应该在发布模式下发生,这assert也是一个错误的工具(抛出异常的确可能也是错误的反应)。但是,我可以肯定的是,实际上assert在C中也用于许多测试,而在C ++中,今天人们会使用异常。
布朗

15

如果发现自己希望在发行版中启用断言,则要求断言执行错误的工作。

断言的要点是它们在发行版中未启用。这允许在开发过程中使用原本必须是脚手架的代码来测试不变式。发布前必须删除的代码。

如果您有什么感觉,即使在发行期间也应该进行测试,则编写测试它的代码。该If throw构造效果很好。如果您想说的东西与其他抛出的东西不同,则只需使用一个描述性异常,该异常可以说出您想说的话。

这并不是说您不能更改断言的使用方式。这是因为这样做并没有给您任何有用的东西,与期望背道而驰,并且使您无法采取明确的方式来执行断言应做的事情。添加在发行版中处于非活动状态的测试。

我不是在讨论断言的特定实现,而是断言的概念。我不想滥用断言或混淆读者。我首先要问为什么是这种方式。为什么没有其他的release_assert?不需要吗?断言在发布中被禁用的背后原理是什么?- 没有人

为什么没有relase_assert?坦率地说,因为断言不足以用于生产。是的,有需求,但是没有什么可以满足需求。哦,确定您可以设计自己的。从机械上来说,throwIf函数只需要一个布尔值和一个异常即可抛出。那可能适合您的需求。但是,您实际上限制了设计。这就是为什么您的语言库中没有诸如断言之类的断言之类的断言让我感到惊讶的原因。当然不是您做不到。其他人都有。但是,对于大多数程序而言,处理出现问题的情况是要完成80%的工作。到目前为止,还没有人向我们展示一种适合所有解决方案的良好尺寸。有效处理这些案件可能会变得复杂。如果我们有一个罐装的release_assert系统无法满足我们的需求,那么我认为它带来的危害更大,那就更好了。您正在要求一个好的抽象,这意味着您不必考虑这个问题。我也想要一个,但看起来好像我们还没到那儿。

为什么在发布中禁用断言?断言是在脚手架代码时代的高峰期创建的。我们必须删除代码,因为知道我们不希望在生产中使用它,但知道我们想在开发中运行以帮助我们发现错误。断言是一种更干净的if (DEBUG)模式,可以让我们保留代码但禁用它。这是在单元测试成为将测试代码与生产代码分离的主要方式之前。如今,即使专家级的单元测试人员也使用断言来阐明期望,并掩盖他们仍然比单元测试做得更好的案例。

为什么不将调试代码留在生产中?因为生产代码不需要使公司感到尴尬,不格式化硬盘,不破坏数据库以及不发送总裁威胁电子邮件。简而言之,能够在无需担心的安全地方编写调试代码是一件很高兴的事情。


2
我想我的问题是:为什么要在发布之前删除此代码?这些检查并不会减少性能,如果失败了,肯定会有一个问题,我希望有一个更直接的错误消息。使用异常而不是断言有什么好处?我可能不希望无关的代码能够做到catch(...)这一点。
没人

2
@Nobody不使用断言满足非断言需求的最大好处是不会使读者感到困惑。如果您不想禁用代码,则不要使用表示它会被禁用的惯用语。这与if (DEBUG)用来控制除调试代码以外的其他东西一样糟糕。在您的情况下,微优化可能毫无意义。但是,不应仅仅因为不需要它而破坏惯用语。
candied_orange

我认为我的意图不够明确。我不是在谈论一个具体的实现,assert而是一个断言的概念。我不想滥用assert或迷惑读者。我首先要问为什么是这种方式。为什么没有额外的release_assert?不需要吗?断言在发布中被禁用的背后原理是什么?
没人

2
You're asking for a good abstraction.我不确定。我主要想处理无法恢复且永远不会发生的问题。结合使用Because production code needs to [...] not format the hard drive [...],我会说对于不变式被破坏的情况,我会随时主张通过UB释放。
没人

1
@没人抛出异常而不捕获异常,可以解决“无法恢复的问题”。您可以注意到,它们永远不会出现在异常的消息文本中。
candied_orange

4

断言是一种调试工具,而不是防御性编程技术。如果要在所有情况下执行验证,请通过编写条件语句来执行验证-或创建自己的宏以减少样板。


4

assert是一种文档形式,例如注释。像评论一样,您通常不会将其运送给客户-它们不属于发布代码。

但是注释的问题在于,它们可能会过时,但仍留在其中。这就是断言很好的原因-在调试模式下检查了它们。当断言变得过时时,您会迅速发现它,并且仍然会知道如何修复断言。那条评论在三年前变得过时了?任何人的猜测。


1
因此,在失败的不变式发生之前中止失败是不是有效的用例?
没人

3

如果您不希望关闭“断言”,请随时编写一个具有类似效果的简单函数:

void fail_if(bool b) {if(!b) std::abort();}

也就是说,assert用于您确实希望它们在出厂产品中消失的测试。如果您希望该测试成为程序定义的行为的一部分,那assert就是错误的工具。


1
我对此很清楚。问题更多地在于为什么默认方式如此。
没人

3

断言assert应该做什么,它做什么都没有意义。如果需要其他功能,请编写自己的函数。例如,我有Assert在调试器中停止运行或不执行任何操作,我有AssertFatal将使应用程序崩溃,我有bool函数Asserted和AssertionFailed声明并返回结果,以便我可以声明和处理这种情况。

对于任何意外的问题,您需要确定对开发人员和用户而言最佳的处理方式是什么。


我并不是要争论断言应该做什么。我对此问题的动机是找到断言不在发布版本中的原因,因为我将要编写自己的文章并希望收集有关用例,期望和陷阱的信息。
没人

嗯,不是verify(cond)断言并返回结果的正常方法吗?
Deduplicator

1
我使用Ruby而不是C进行编码,但是我不同意上面的大多数答案,因为作为防御性编程技术,通过打印回溯来停止程序对我来说很好用.......我的评论不适合最大评论大小(以字符为单位),所以我在下面写了另一个答案。
Nakilon

2

正如其他人所指出的那样,这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::atvector::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!");自然很便宜,因为检查首先是在特殊路径中执行的,而在正常执行路径中不会花费任何费用。


我的意图不是在第三方代码中启用断言,请参见澄清性编辑。我的评论中的引用忽略了我的问题的上下文:Additionally, the performance argument for me only counts when it is a measurable problem.因此,该想法是根据具体情况决定何时从发布中删除声明。
没人

最后一个注释的问题之一是,标准库使用与使用宏时assert所依赖的相同的_DEBUG/NDEBUG预处理程序定义assert。因此,没有办法仅针对一个代码选择性地启用断言,而不同时为标准库启用它,例如,假设它使用标准库。

您可以单独禁用STL迭代器检查,从而禁用某些断言,但不能全部断言。

它基本上是一个粗糙的工具,不是粒度化的,并且始终具有在发布中被禁用的设计意图,因此您的团队伙伴可能会在关键区域使用它,而前提是它不会影响发布的性能,因此标准库会使用该工具。在关键区域中使用这种方式,其他第三方库以这种方式使用它,等等。当您需要某些功能时,可以更具体地禁用/启用一个代码段,而不是其他代码,我们将重新考虑非assert

vector::operator[]并且vector::at,例如,如果断言已启用在发布版本,而且会是毫无意义的使用将有几乎相同的性能operator[]了从性能的角度来看,因为它会做的边界检查反正。

2

我使用Ruby而不是C / C ++进行编码,因此我不会谈论断言和异常之间的区别,但是我想将其视为停止运行时事情。我不同意上面的大多数答案,因为通过打印回溯来停止程序对于我来说是一种防御性编程技术,效果很好
如果有一种方法可以断言例程(绝对不管其语法如何编写,以及编程语言或dsl中是否使用或存在“ assert”一词),则意味着应该做一些工作,并且产品立即从“已发布,已准备就绪使用”回到“需要补丁”-现在可以将其重写为真正的异常处理或修复导致出现错误数据的错误。

我的意思是,Assert不是您应该经常使用并经常调用的东西,它是一个停止信号,指示您应该尽力使它不再发生。并表示,“发布版本不应该称”好像是说“发布版本不应该有错误” -伙计,这几乎是不可能的,解决它。
或者将它们视为“最终用户进行和运行的单元测试失败”。您无法预测用户将要使用程序执行的所有操作,但是如果太严重了会出错,它应该停止运行-这类似于构建构建管道的方式-您停止了该过程并且不发布,您是否?断言强制用户停止,报告并等待您的帮助。

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.