在C++中使用assert()是不良实践吗?

112
我倾向于在我的C++代码中添加大量的断言来使调试更容易,而不影响发布版本的性能。现在,assert是一个纯C宏,没有考虑到C++机制。
另一方面,C++定义了std::logic_error,用于在程序逻辑出错的情况下抛出异常(因此命名为“逻辑错误”)。手动抛出实例可能是完美的、更符合C++风格的assert替代方法。
问题在于,assertabort都会立即终止程序而不调用析构函数,从而跳过清理工作,而手动抛出异常会增加不必要的运行时成本。解决这个问题的一种方法是创建一个自己的断言宏SAFE_ASSERT,它与C的对应物类似,但在失败时抛出异常。
我能想到三种解决这个问题的方法:
- 坚持使用C的assert。由于程序立即终止,正确卸载更改并不重要。此外,在C++中使用#define与使用C中的情况一样糟糕。 - 抛出异常并在main()中捕获异常。允许代码在程序的任何状态下跳过析构函数是不良实践,必须尽一切可能避免,调用terminate()也是如此。如果抛出异常,则必须捕获它们。 - 抛出异常并让其终止程序。一个异常终止程序是可以接受的,并且由于NDEBUG,这在发布版本中永远不会发生。捕获异常是不必要的,并将内部代码的实现细节暴露给main()。
这个问题有明确的答案吗?有任何专业参考资料吗?
编辑:跳过析构函数当然不是未定义行为。

27
不,真的,“logic_error”是逻辑错误。程序逻辑上的错误称为“bug”。你不能通过抛出异常来解决这些错误。 - R. Martinho Fernandes
5
断言、异常、错误码。每个都有完全不同的用例,你不应该在需要另一个的情况下使用其中任何一个。 - Kerrek SB
7
如果你可以使用static_assert,请确保在适当的地方使用它。 - Flexo
4
@trion 我不明白那有什么帮助。你会抛出 std::bug 吗? - R. Martinho Fernandes
3
@trion说:不要这样做。异常不是用来调试的。有人可能会捕获这个异常。在调用 std::abort() 时不需要担心未定义行为,它只会引发一个信号,导致进程终止。 - Kerrek SB
显示剩余5条评论
5个回答

110
  • 断言用于调试。使用您发布的代码的用户不应该看到它们。如果触发了一个断言,您需要修复代码。

    CWE-617: 可达断言

该产品包含可以被攻击者触发的assert()或类似语句,导致应用程序退出或其他行为比必要更严重。

虽然断言对于捕获逻辑错误和减少达到更严重漏洞条件的机会是有益的,但它仍可能导致服务拒绝攻击 (DoS)。

例如,如果服务器处理多个同时连接,并且在单个连接中出现了一个断言(assert())导致所有其他连接都被中断,这就是可达断言,会导致 DoS 攻击。

  • 异常用于特殊情况。如果遇到异常,用户可能无法完成想要的操作,但可以在其他地方恢复。

  • 错误处理用于正常程序流程。例如,如果提示用户输入数字并且得到无法解析的内容,那是正常的,因为用户输入不在您的控制之下,您必须始终处理所有可能的情况。 (例如,在输入有效内容前循环,每次在其间说“抱歉,请重试”)。


1
来寻找这个re assert的人;任何形式的assert通过到生产代码都指向了糟糕的设计和QA。调用assert的点应该是优雅地处理错误条件的地方。(我从不使用assert)。至于异常,我知道的唯一用例是当ctor可能失败时,所有其他情况都是用于正常的错误处理。 - slashmais
10
这句话的意思是,虽然作者的想法值得赞扬,但除非您发布了完美、无错的代码,否则我认为断言语句(即使会让用户程序崩溃)比未定义的行为更可取。在复杂系统中,错误是难以避免的,而使用断言语句可以帮助您确定和诊断错误发生的位置。 - Kerrek SB
@KerrekSB 我更倾向于使用异常而不是断言。至少代码有机会放弃失败的分支并执行其他有用的操作。最起码,如果你正在使用RAII,所有打开文件的缓冲区都将被正确地刷新。 - user4266696
我从不使用assert。以前,图灵认为它们非常重要,他似乎对计算机知识很了解,所以我选择听从他的意见。 - Ron Burk

88

断言在C++代码中非常适用。异常和其他错误处理机制并不是为了解决与断言相同的问题。

错误处理用于在存在恢复或向用户报告错误的潜力时。例如,如果尝试读取输入文件时出现错误,则可能需要采取一些措施。错误可能由错误导致,但也可能仅仅是给定输入的合适输出。

断言用于检查当 API 未被正常检查时是否满足其要求,或者用于检查开发人员认为已经通过构造保证的内容。例如,如果算法要求有序输入,您通常不会检查它,但是您可以使用断言来检查它,以便调试版本能够标记这种错误。断言应始终指示程序的错误操作。


如果编写的程序关闭不干净可能会导致问题,则应避免使用断言。在C ++语言严格意义上来说,未定义的行为不属于此类问题,因为触发断言可能已经是未定义行为的结果,或者是违反某些其他要求,这可能会妨碍某些清理工作正常进行。

此外,如果您将断言实现为异常,则即使违反了断言的目的,它也可能被捕获和“处理”。


1
我不确定答案中是否明确说明了这一点,所以我在这里说明一下:对于任何涉及用户输入且无法在编写代码时确定的内容,都不应使用断言。如果用户将 3 而不是 1 传递给您的代码,则通常不应触发断言。断言只是程序员错误,而不是库或应用程序的用户错误。 - S.S. Anne

14

断言可用于验证内部实现的不变性,例如某个方法执行前或执行后的内部状态等。如果断言失败,这意味着程序逻辑出现了问题,您无法从中恢复。在这种情况下,您最好尽快中断程序而不将异常传递给用户。值得一提的是,在Linux系统上使用断言非常方便,因为进程终止会生成核心转储文件,您可以轻松调查堆栈跟踪和变量。与异常信息相比,这更有助于理解逻辑错误。


我有类似的方法。 我使用断言来处理应该局部正确的逻辑(例如循环不变量)。 异常是用于在代码中被一个非局部(外部)情况强制施加逻辑错误的地方。 - spraff
如果一个断言失败了,那么意味着程序的某个部分逻辑是错误的。一个失败的断言并不一定意味着什么都做不了。一个损坏的插件可能不应该中止整个文字处理器。 - user4266696

13

因为调用abort()而不运行析构函数并不是未定义的行为!

如果是这样的话,那么调用std::terminate()也将是未定义的行为,那么提供它的意义在哪里呢?

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


1
我认为abort()是用于立即终止程序的。你说得对,断言并不是用于错误处理,但是assert尝试通过中止来处理错误。如果可以的话,难道你不应该抛出异常并让调用者处理错误吗?毕竟,调用者更有能力确定一个函数的失败是否使得其他操作不值得进行。也许调用者正在尝试做三件不相关的事情,并且仍然可以完成其他两个任务,只需放弃这一个任务。 - user4266696
assert 被定义为在条件为假时调用 abort。至于抛出异常,不,那并不总是合适的。有些事情无法由调用者处理。调用者无法确定第三方库函数中的逻辑错误是否可恢复,或者是否可以修复损坏的数据。 - Jonathan Wakely

6
在我看来,断言是用于检查违反条件的情况,如果违反了条件,那么其他所有东西都没有意义。因此,你不能从中恢复,或者说恢复无关紧要。
我将它们分为两类:
开发人员的错误(例如返回负值的概率函数):
float probability() { return -1.0; } assert(probability() >= 0.0)
机器出现故障(例如运行程序的机器非常错误):
int x = 1; assert(x > 0);
这些都是微不足道的例子,但并不离真实情况太远。例如,考虑返回负索引以与向量一起使用的天真算法。或嵌入式程序在自定义硬件中。或者只是因为“狗屎”发生了。
如果存在这样的开发错误,你就不应该对任何恢复或错误处理机制感到自信。同样适用于硬件错误。

1
assert(probability() >= 0.0) - Elliott

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接