章节目录

断言

本节阅读量:

在接受参数的函数中,调用方可以传入语法上有效但语义上无意义的参数。例如,上一课中的示例函数:

1
2
3
4
5
6
7
void printDivision(int x, int y)
{
    if (y != 0)
        std::cout << static_cast<double>(x) / y;
    else
        std::cerr << "Error: Could not divide by zero\n";
}

该函数执行显式检查,以查看y是否为0,因为除以零是语义错误,如果执行会导致程序崩溃。

上一课中,我们讨论了两种处理此类问题的方法:停止程序,或跳过有问题的语句。

然而,这两种选择都有问题。

如果程序由于错误而跳过语句,那么它本质上是在静默失败。特别是在编写和调试程序时,静默故障是不好的,因为它们会掩盖真正的问题。即使打印了错误消息,该消息也可能淹没在其他程序输出中。而且,错误消息在哪里生成、触发错误消息的条件是如何发生的,也可能并不明显。有些函数可能被调用几十次或几百次,如果这些调用中只有一次产生问题,就很难知道是哪一次。

如果程序终止(通过std::exit),那么我们将丢失调用堆栈,以及任何可能帮助定位问题的调试信息。对于这种情况,std::abort是更好的选择,因为开发人员通常可以选择从程序中止的位置开始调试。


前置条件、不变量和后置条件

在编程中,前置条件是在执行某些代码段(通常是函数体)之前必须为true的条件。在上例中,检查y != 0,是为了确保在除以y之前,y具有非零值。

函数的前置条件最好放在函数顶部,如果不满足前置条件,则使用提前返回返回给调用方。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void printDivision(int x, int y)
{
    if (y == 0) // 校验前置条件 
    {
        std::cerr << "Error: Could not divide by zero\n";
        return; // 返回给调用方
    }

    // 这里一定 y != 0
    std::cout << static_cast<double>(x) / y;
}

不变量是在执行代码的某些部分时必须为true的条件。这通常用于循环,其中循环体只在不变量为true时执行。

类似地,后置条件是在执行代码的某些部分后必须为true的条件。上面示例中的函数没有任何后置条件。


断言

使用条件语句检测无效参数(或验证其他类型的假设),然后打印错误消息并终止程序,是检测问题的常见方法。C++为此提供了一种快捷方式。

断言是一个表达式。如果表达式的计算结果为true,则断言语句不执行任何操作。如果条件表达式的计算结果为false,则显示错误消息并终止程序(通过std::abort)。该错误消息通常包含表达式文本、代码文件名和断言所在行号。这不仅能让我们很容易知道问题是什么,还能知道问题发生在代码中的什么位置。这对调试非常有帮助。

在C++中,运行时断言是通过断言预处理器宏实现的,该宏位于头文件中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <cassert> // for assert()
#include <cmath> // for std::sqrt
#include <iostream>

double calculateTimeUntilObjectHitsGround(double initialHeight, double gravity)
{
  assert(gravity > 0.0); // 重力一定为正
 
  if (initialHeight <= 0.0)
  {
    // 东西已经落在地上了
    return 0.0;
  }
 
  return std::sqrt((2.0 * initialHeight) / gravity);
}

int main()
{
  std::cout << "Took " << calculateTimeUntilObjectHitsGround(100.0, -9.8) << " second(s)\n";

  return 0;
}

当程序调用calculateTimeUntilObjectHitsGround(100.0, -9.8)时,assert(gravity > 0.0)的计算结果将为false,这会触发断言。程序将打印类似如下内容的消息:

1
dropsimulator: src/main.cpp:6: double calculateTimeUntilObjectHitsGround(double, double): Assertion 'gravity > 0.0' failed.

实际消息因使用的编译器而异。

尽管断言最常用于验证函数参数,但它们也可以用在任何希望验证某些内容是否正确的地方。

尽管我们以前告诉过您不要使用预处理器宏,但断言是少数被认为可以使用的预处理器宏之一。我们鼓励您在代码中自由使用断言语句。


使断言语句更具描述性

有时断言表达式并不具有很强的描述性。考虑以下语句:

1
assert(found);

如果触发此断言,则断言会显示:

1
Assertion failed: found, file C:\\VCProjects\\Test.cpp, line 34

这意味着什么?显然found的值是false(因为断言被触发),但为什么会这样?您必须查看代码才能确定。

幸运的是,有一个小技巧可以使断言语句更具描述性。只需添加由逻辑AND连接的字符串文本:

1
assert(found && "Car could not be found in database");

这样做的原因是:字符串文本总是计算为布尔true。因此,如果found为false,则false && true为false。如果found为true,则true && true为true。因此,对字符串文本进行逻辑“与”运算不会影响断言的计算结果。

当断言触发时,字符串文本会包含在断言消息中:

1
Assertion failed: found && "Car could not be found in database", file C:\\VCProjects\\Test.cpp, line 34

这会提供一些关于出错原因的额外背景。


断言与错误处理

断言和错误处理非常相似,因此它们的目的可能会被混淆。让我们澄清一下:

断言的目标是记录不应该发生的事情,以捕获编程错误。如果那件事真的发生了,那么说明程序员在某处犯了错误,并且该错误可以被识别和修复。断言不允许从错误中恢复(毕竟,如果某些事情永远不应该发生,就不需要从中恢复),并且程序不会产生友好的错误消息。

另一方面,错误处理旨在优雅地处理可能发生问题的情况(尽管这些情况可能很少见)。这些问题可以是可恢复的,也可以是不可恢复的,但应该始终假设程序用户可能会遇到它们。

断言有时也用于记录未实现功能的情况,因为编写代码时还不需要实现它们:

1
assert(moved && "Need to handle case where student was just moved to another classroom");

这样,如果代码未来的使用者确实遇到需要这种情况的场景,代码将失败,并显示有用的错误消息,然后程序员可以决定如何实现这种情况。


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采用以下形式:

1
static_assert(条件表达式, 诊断信息)

如果条件不为真,则打印诊断消息。下面是使用static_assert确保类型具有特定大小的示例:

1
2
3
4
5
6
7
static_assert(sizeof(long) == 8, "long must be 8 bytes");
static_assert(sizeof(int) >= 4, "int must be at least 4 bytes");

int main()
{
	return 0;
} 

在作者的机器上编译时,编译器会报错:

1
1>c:\consoleapplication1\main.cpp(19): error C2338: long must be 8 bytes

关于static_assert的一些有用点:

  1. 由于static_assert在编译期计算,因此条件表达式必须是常量表达式。
  2. static_assert可以放在代码文件中的任何位置(甚至在全局命名空间中)。
  3. static_assert不会在发布版本中被禁用。

在C++17之前,必须将诊断消息作为第二个参数提供。在C++17及之后,提供诊断消息是可选的。


9.4 std::cin和处理无效输入

上一节

9.6 第9章总结

下一节