断言
本节阅读量:在接受参数的函数中,调用方可以传入语法上有效但语义上无意义的参数。例如,上一课中的示例函数:
|
|
该函数执行显式检查,以查看y是否为0,因为除以零是语义错误,如果执行会导致程序崩溃。
上一课中,我们讨论了两种处理此类问题的方法:停止程序,或跳过有问题的语句。
然而,这两种选择都有问题。
如果程序由于错误而跳过语句,那么它本质上是在静默失败。特别是在编写和调试程序时,静默故障是不好的,因为它们会掩盖真正的问题。即使打印了错误消息,该消息也可能淹没在其他程序输出中。而且,错误消息在哪里生成、触发错误消息的条件是如何发生的,也可能并不明显。有些函数可能被调用几十次或几百次,如果这些调用中只有一次产生问题,就很难知道是哪一次。
如果程序终止(通过std::exit),那么我们将丢失调用堆栈,以及任何可能帮助定位问题的调试信息。对于这种情况,std::abort是更好的选择,因为开发人员通常可以选择从程序中止的位置开始调试。
前置条件、不变量和后置条件
在编程中,前置条件是在执行某些代码段(通常是函数体)之前必须为true的条件。在上例中,检查y != 0,是为了确保在除以y之前,y具有非零值。
函数的前置条件最好放在函数顶部,如果不满足前置条件,则使用提前返回返回给调用方。例如:
|
|
不变量是在执行代码的某些部分时必须为true的条件。这通常用于循环,其中循环体只在不变量为true时执行。
类似地,后置条件是在执行代码的某些部分后必须为true的条件。上面示例中的函数没有任何后置条件。
断言
使用条件语句检测无效参数(或验证其他类型的假设),然后打印错误消息并终止程序,是检测问题的常见方法。C++为此提供了一种快捷方式。
断言是一个表达式。如果表达式的计算结果为true,则断言语句不执行任何操作。如果条件表达式的计算结果为false,则显示错误消息并终止程序(通过std::abort)。该错误消息通常包含表达式文本、代码文件名和断言所在行号。这不仅能让我们很容易知道问题是什么,还能知道问题发生在代码中的什么位置。这对调试非常有帮助。
在C++中,运行时断言是通过断言预处理器宏实现的,该宏位于头文件中。
|
|
当程序调用calculateTimeUntilObjectHitsGround(100.0, -9.8)时,assert(gravity > 0.0)的计算结果将为false,这会触发断言。程序将打印类似如下内容的消息:
|
|
实际消息因使用的编译器而异。
尽管断言最常用于验证函数参数,但它们也可以用在任何希望验证某些内容是否正确的地方。
尽管我们以前告诉过您不要使用预处理器宏,但断言是少数被认为可以使用的预处理器宏之一。我们鼓励您在代码中自由使用断言语句。
关键点
当断言的计算结果为false时,程序将立即停止。这使您有机会使用调试工具来检查程序的状态,并确定断言失败的原因。然后您可以找到并解决问题。
如果发生了错误,但没有设置断言检查,这样的错误可能会导致程序稍后才发生故障。在这种情况下,很难确定哪里出了问题,或者问题的根本原因是什么。
使断言语句更具描述性
有时断言表达式并不具有很强的描述性。考虑以下语句:
|
|
如果触发此断言,则断言会显示:
|
|
这意味着什么?显然found的值是false(因为断言被触发),但为什么会这样?您必须查看代码才能确定。
幸运的是,有一个小技巧可以使断言语句更具描述性。只需添加由逻辑AND连接的字符串文本:
|
|
这样做的原因是:字符串文本总是计算为布尔true。因此,如果found为false,则false && true为false。如果found为true,则true && true为true。因此,对字符串文本进行逻辑“与”运算不会影响断言的计算结果。
当断言触发时,字符串文本会包含在断言消息中:
|
|
这会提供一些关于出错原因的额外背景。
断言与错误处理
断言和错误处理非常相似,因此它们的目的可能会被混淆。让我们澄清一下:
断言的目标是记录不应该发生的事情,以捕获编程错误。如果那件事真的发生了,那么说明程序员在某处犯了错误,并且该错误可以被识别和修复。断言不允许从错误中恢复(毕竟,如果某些事情永远不应该发生,就不需要从中恢复),并且程序不会产生友好的错误消息。
另一方面,错误处理旨在优雅地处理可能发生问题的情况(尽管这些情况可能很少见)。这些问题可以是可恢复的,也可以是不可恢复的,但应该始终假设程序用户可能会遇到它们。
断言有时也用于记录未实现功能的情况,因为编写代码时还不需要实现它们:
|
|
这样,如果代码未来的使用者确实遇到需要这种情况的场景,代码将失败,并显示有用的错误消息,然后程序员可以决定如何实现这种情况。
最佳实践
使用断言来记录逻辑上不可能的情况。
NDEBUG
每次检查断言条件时,断言宏都会产生较小的性能开销。此外,(理想情况下)生产代码中永远不会遇到断言(因为您的代码应该已经经过彻底测试)。因此,许多开发人员更喜欢只在调试构建中使用断言。C++提供了一种在生产代码中关闭断言的方法。如果定义了宏NDEBUG,则断言宏会被禁用。
一些IDE默认会把NDEBUG设置为发布配置项目设置的一部分。例如,在Visual Studio中,项目配置默认设置以下预处理器定义:WIN32;NDEBUG;_CONSOLE。如果您正在使用Visual Studio,并且希望在发布版本中触发断言,则需要从该设置中删除NDEBUG。
如果您使用的IDE或构建系统没有在发布配置中自动定义NDEBUG,则需要将其手动添加到项目或编译设置中。
一些断言限制和警告
断言有一些缺陷和限制。首先,断言语句本身可能编写不正确。如果发生这种情况,断言要么报告不存在的错误,要么不报告实际存在的错误。
其次,您的断言应该没有副作用——也就是说,程序在有断言和没有断言的情况下应该运行相同。否则,您在调试配置中测试的内容将与发布配置中的内容不同。
还要注意,abort()函数会立即终止程序,没有机会进行任何进一步清理(例如关闭文件或数据库)。因此,断言应该只在程序意外终止且不太可能造成损坏的情况下使用。
static_assert
C++还有另一种类型的断言,称为static_assert。static_assert是在编译时而不是运行时检查的断言,失败的static_assert会导致编译错误。与在头文件中声明的assert不同,static_assert是一个关键字,因此不需要包含任何头文件即可使用。
static_assert采用以下形式:
|
|
如果条件不为真,则打印诊断消息。下面是使用static_assert确保类型具有特定大小的示例:
|
|
在作者的机器上编译时,编译器会报错:
|
|
关于static_assert的一些有用点:
- 由于static_assert在编译期计算,因此条件表达式必须是常量表达式。
- static_assert可以放在代码文件中的任何位置(甚至在全局命名空间中)。
- static_assert不会在发布版本中被禁用。
在C++17之前,必须将诊断消息作为第二个参数提供。在C++17及之后,提供诊断消息是可选的。