在C ++中使用assert()是不好的做法吗?


93

我倾向于在我的C ++代码中添加很多断言,以使调试更加容易,而又不影响发行版的性能。现在,assert是一个纯C语言宏,它没有考虑C ++机制。

另一方面std::logic_error,C ++定义了,它是在程序逻辑中有错误(因此而得名)的情况下抛出的。抛出一个实例可能是的完美,更C ++的替代方法assert

问题是,assertabort两者立即终止程序,而无需调用析构函数,因此跳过清理,而抛出异常手动增加了不必要的运行时成本。解决该问题的一种方法是创建一个自己的断言宏SAFE_ASSERT,该宏的工作原理与C语言的对等宏相同,但会在失败时引发异常。

对于这个问题,我可以想到三点意见:

  • 坚持C的主张。由于该程序会立即终止,因此是否正确展开更改并不重要。同样,#define在C ++中使用s同样不好。
  • 引发异常并将其捕获到main()中。允许代码在程序的任何状态下跳过析构函数都是一种不好的做法,必须不惜一切代价避免这样做,对终止()的调用也应避免。如果引发异常,则必须将其捕获。
  • 引发异常并使其终止程序。 终止程序的异常是可以的,并且由于NDEBUG,这在发行版本中永远不会发生。捕获是不必要的,并且可以向公开内部代码的实现细节main()

这个问题有明确的答案吗?有专业参考吗?

编辑:跳过析构函数当然不是不确定的行为。


22
不,确实logic_error是逻辑错误。程序逻辑中的错误称为错误。您不会通过抛出异常来解决错误。
R. Martinho Fernandes

4
断言,异常,错误代码。每个案例都有一个完全不同的用例,您不应在需要另一个案例的情况下使用。
Kerrek SB 2012年

5
static_assert如果可用,请确保在适当的地方使用。
Flexo

4
@trion我看不出有什么帮助。你会扔std::bug吗?
R. Martinho Fernandes

3
@trion:不要那样做。异常不适用于调试。可能有人正在捕获异常。呼叫时无需担心UB std::abort();它只会发出一个导致进程终止的信号。
Kerrek SB 2012年

Answers:


73

断言在C ++代码中完全合适。异常和其他错误处理机制与断言实际上并不是同一目的。

错误处理用于有可能将错误很好地恢复或报告给用户的情况。例如,如果尝试读取输入文件时出错,则您可能需要对此做一些事情。错误可能是由错误引起的,但也可能只是给定输入的适当输出。

断言用于诸如在通常情况下不检查API时检查是否满足API要求之类的事情,或用于检查开发人员认为由构造保证的事情。例如,如果算法需要排序的输入,则通常不会进行检查,但是您可能有一个断言来对其进行检查,以便debug builds标记此类错误。断言应始终指示程序运行不正确。


如果您正在编写一个程序,其中不正常的关机可能导致问题,那么您可能要避免断言。从严格意义上来说,就C ++语言而言,未定义的行为不属于此类问题,因为命中断言可能已经是未定义行为的结果,或者违反了某些其他要求(可能会导致某些清理工作无法正常进行)的结果。

同样,如果您根据异常实现断言,那么即使它与断言的真正目的相矛盾,也有可能捕获并“处理”该断言。


1
我不完全确定这是否在答案中特别说明,因此在此说明:对于编写代码时无法确定的涉及用户输入的任何内容,请勿使用断言。如果用户通过3而不是1通过您的代码,则通常不应触发断言。断言仅是程序员错误,而不是库用户或应用程序错误。
SS安妮

101
  • 断言用于调试。您提供的代码的用户永远都不会看到它们。如果命中了一个断言,则您的代码需要修复。

  • 例外是在特殊情况下。如果遇到一个问题,用户将无法做她想做的事,但可能可以在其他地方继续工作。

  • 错误处理适用于常规程序流程。例如,如果您提示用户输入数字并得到无法解析的信息,那是正常现象,因为用户输入不受您的控制,因此您必须始终处理所有可能的情况。(例如,循环播放,直到您输入有效的内容,然后在两者之间说“对不起,再试一次”。)


1
来寻找这个断言;传递到生产代码的任何形式的断言都意味着不良的设计和质量保证。断言的调用点是应该适当处理错误条件的地方。(我从不使用断言的)。至于异常,我知道的唯一用例是ctor可能失败时,所有其他用例都是用于常规错误处理。
slashmais '16

5
@slashmais:这种观点是值得称赞的,但是除非您发布的是完美的,没有错误的代码,否则我会发现断言(甚至使用户崩溃的断言)比未定义的行为更可取。错误发生在复杂的系统中,并且有了断言,您可以查看和诊断错误发生的位置。
Kerrek SB

@KerrekSB我宁愿使用断言而不是断言。至少代码有机会丢弃失败的分支并做其他有用的事情。至少,如果您使用的是RAII,则用于打开文件的所有缓冲区都将正确刷新。
daemonspring '17

14

断言可用于验证内部实现的不变性,例如在执行某种方法之前或之后的内部状态等。如果断言失败,则实际上意味着程序的逻辑已损坏,您无法从中恢复。在这种情况下,您最好的办法是在不将异常传递给用户的情况下尽快中断。关于断言的真正好处(至少在Linux上是这样)的是,核心转储是进程终止的结果,因此您可以轻松地研究堆栈跟踪和变量。对于理解逻辑故障,这比异常消息有用得多。


我有类似的方法。我将断言用于逻辑上可能在本地正确的声明(例如,循环不变式)。异常是由于非本地(外部)情况在代码上强加逻辑错误的情况。
spraff '16

如果断言失败,则意味着程序的一部分逻辑被破坏了。断言失败不一定表示无法完成任何事情。损坏的插件可能不应终止整个文字处理器。
daemonspring '17

13

由于所有的abort()而导致未运行析构函数的行为不是未定义的!

如果是这样,那么调用std::terminate()也将是不确定的行为,那么提供它的意义何在?

assert() 在C ++中和在C中一样有用。断言不是用于错误处理,而是用于立即中止程序。


1
我想说的abort()是立即终止程序。没错,断言不是用于错误处理的,但是断言确实尝试通过中止来处理错误。您是否应该抛出异常并让调用者处理错误(如果可以的话)?毕竟,调用者可以更好地确定某个函数的失败是否使其不值得做其他任何事情。也许呼叫者正在尝试做三件无关的事情,并且仍然可以完成其他两项工作,而只是丢掉这一项。
daemonspring '17

并且assert定义为调用abort(条件为假时)。至于抛出异常,不总是这样。呼叫者无法处理某些事情。调用者无法确定第三方库函数中的逻辑错误是否可恢复,或者是否可以修复损坏的数据。
乔纳森·威克利

6

恕我直言,断言用于检查条件,如果违反,则使其他所有内容都没有意义。因此,您无法从它们中恢复,或者恢复是无关紧要的。

我将它们分为两类:

  • 开发者的过失(例如,返回负值的概率函数):

浮点概率(){return -1.0; }

断言(概率()> = 0.0)

  • 机器损坏(例如,运行程序的机器非常错误):

int x = 1;

断言(x> 0);

这些都是微不足道的例子,但与现实相距不远。例如,考虑一下天真算法,该算法返回负索引以用于向量。或自定义硬件中的嵌入式程序。或者更确切地说是因为发生了

并且,如果存在此类开发错误,您应该对所实施的任何恢复或错误处理机制不抱有信心。硬件错误也是如此。


1
assert(probability()> = 0.0)
Elliott
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.