9-6 assert与static_assert
在接受形参的函数中,调用者可能传递语法上正确但语义上无意义的实参。例如,在上一课(9.4——检测和处理错误)中,我们展示了以下示例函数:
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是更优选择——开发者通常能在程序中止处启动调试流程。
先决条件、不变条件和后置条件
在编程中,先决条件precondition是指在执行某段代码(通常是函数体)之前必须成立的任何条件。在前例中,我们检查 y != 0 的操作就是先决条件,它确保在 y 除法操作前 y 具有非零值。
函数的先决条件最好置于函数开头,若条件未满足则通过提前返回跳转至调用方。例如:
void printDivision(int x, int y)
{
if (y == 0) // handle
{
std::cerr << "Error: Could not divide by zero\n";
return; // bounce the user back to the caller
}
// We now know that y != 0
std::cout << static_cast<double>(x) / y;
}
可选阅读
这种模式有时被称为“弹出模式”,因为一旦检测到错误,程序就会立即被弹出函数。
弹出模式具有两大优势:
- 所有测试用例都位于最前端,且测试用例与处理错误的代码紧邻放置。
- 最终实现更少的嵌套层级。
以下是此内容的非弹出式版本:
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"; return; // bounce the user back to the caller } }此版本明显更差,因为测试用例与错误处理代码的分离程度更高,且嵌套层级更深。
不变量invariant是指在代码某段执行期间必须成立的条件。它常用于循环中,此时循环体仅在不变量成立时才会执行。
对于高级读者
我们在第14.2节——类介绍中讨论了一种常见的不变量类型,称为“类不变量(class invariant)”。
同样地,后置条件postcondition是指在执行某段代码之后必须成立的条件。我们的函数没有后置条件。
断言
使用条件语句检测无效形参(或验证其他假设),同时打印错误信息并终止程序,是如此常见的问题检测方法,以至于C++为此提供了快捷实现方式。
断言assertion是一种表达式,除非程序存在错误,否则该表达式为真。若表达式评估结果为真,断言语句将不执行任何操作。若条件表达式评估为假,则会显示错误信息并终止程序(通过 std::abort 实现)。该错误信息通常包含失败表达式的文本内容,以及代码文件名和断言所在的行号。这使得开发者不仅能轻松识别问题本质,更能精准定位代码中的问题位置,极大提升调试效率。
关键洞察
断言用于在开发和调试过程中检测错误。
当断言评估为假时,程序会立即停止运行。这使您能够利用调试工具检查程序状态,并确定断言失败的原因。通过逆向排查,您可快速定位并修复问题。
若无断言检测错误并触发中止,此类错误很可能导致程序后续运行异常。此时往往难以定位故障点,更难以追溯问题的根本原因。
在 C++ 中,运行时断言是通过 assert 预处理宏实现的,该宏位于
#include <cassert> // for assert()
#include <cmath> // for std::sqrt
#include <iostream>
double calculateTimeUntilObjectHitsGround(double initialHeight, double gravity)
{
assert(gravity > 0.0); // The object won't reach the ground unless there is positive gravity.
if (initialHeight <= 0.0)
{
// The object is already on the ground. Or buried.
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,从而触发断言。这将打印类似以下的消息:

实际显示的提示信息会因所用编译器而异。
虽然断言最常用于验证函数参数,但它们可在任何需要验证条件成立的场景中使用。
尽管我们此前建议避免使用预处理器宏,但断言是少数被认为可接受的预处理器宏之一。我们鼓励您在代码中广泛使用断言语句。
关键洞见
断言优于注释,因为它们既能记录条件又能强制执行条件。当代码变更而注释未更新时,注释可能失效。过时的断言会引发代码正确性问题,因此开发者更倾向于及时更新。
使断言语句更具描述性
有时断言表达式不够具体。请看以下语句:
assert(found);
如果此断言被触发,断言将显示:
Assertion failed: found, file C:\\VCProjects\\Test.cpp, line 34
这到底是什么意思?显然断言触发了,说明检测到错误,但究竟没找到什么?你得去代码里找才能确定。
幸运的是,有个小技巧能让断言语句更具描述性:只需添加字符串字面量,并用逻辑与运算符连接:
assert(found && "Car could not be found in database");
其原理如下:字符串常量始终求值为布尔值 true。因此若 found 为 false,则 false && true 结果为 false;若 found 为 true,则 true && true 结果为 true。故对字符串常量进行逻辑与运算不会影响断言的求值结果。
但当断言触发时,字符串常量将被包含在断言消息中:
Assertion failed: found && "Car could not be found in database", file C:\\VCProjects\\Test.cpp, line 34
这为你提供了更多关于问题出在哪里的背景信息。
使用断言标记未实现的功能
断言有时也用于记录因编写代码时无需实现而未实现的功能:
assert(moved && "Need to handle case where student was just moved to another classroom");
这样一来,当开发人员遇到需要这种情况时,代码会以有用的错误信息失败,程序员随后就能确定如何实现该情况。
NDEBUG
断言宏每次检查断言条件时都会带来微小的性能开销。此外,断言在生产代码中(理想情况下)不应出现(因为代码理应经过充分测试)。因此,大多数开发者倾向于仅在调试版本中启用断言。C++ 提供内置机制可在生产代码中禁用断言:若定义预处理器宏 NDEBUG,则 assert 宏将失效。
多数 IDE 会在发布配置的项目设置中默认启用 NDEBUG。例如在 Visual Studio 中,项目级别会设置以下预处理器定义:WIN32;NDEBUG;_CONSOLE。若使用 Visual Studio 且希望在发布构建中触发断言,需从该设置中移除 NDEBUG。
若使用的 IDE 或构建系统未在发布配置中自动定义 NDEBUG,则需在项目或编译设置中手动添加。
提示
为测试目的,您可以在特定翻译单元内启用或禁用断言。操作方法:在任何#include指令之前before单独添加以下定义之一:#define NDEBUG(禁用断言)或#undef NDEBUG(启用断言)。请确保该行末尾不添加分号。
例如:#define NDEBUG // disable asserts (must be placed before any #includes) #include <cassert> #include <iostream> int main() { assert(false); // won't trigger since asserts have been disabled in this translation unit std::cout << "Hello, world!\n"; return 0; }
static_assert
C++还提供另一种名为static_assert的断言类型。static_assert是在编译时而非运行时进行检查的断言,其失败将导致编译错误。与声明于
static_assert采用以下形式:
static_assert(condition, diagnostic_message)
如果条件不成立,则打印诊断信息。以下是使用 static_assert 确保类型具有特定大小的示例:
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>c:\consoleapplication1\main.cpp(19): error C2338: long must be 8 bytes
关于 static_assert 的几点实用说明:
- 由于 static_assert 由编译器进行求值,其条件必须是常量表达式。
- static_assert 可放置在代码文件的任意位置(甚至全局命名空间中)。
- static_assert 在发布构建中不会被禁用(与普通 assert 不同)。
- 由于求值由编译器完成,static_assert 不存在运行时开销。
在 C++17 之前,诊断信息必须作为第二个参数提供。自 C++17 起,提供诊断信息成为可选操作。
最佳实践
尽可能优先使用 static_assert 替代 assert()。
断言与错误处理
断言和错误处理足够相似,以至于它们的目的容易被混淆,因此我们需要澄清。
断言用于在开发过程中检测编程错误,通过记录关于不应发生情况的假设来实现。如果这些情况确实发生,则归咎于程序员的失误。断言不支持错误恢复(毕竟不应发生的情况无需恢复)。由于断言在正式版本中通常会被编译移除,可大量使用而无需顾虑性能问题,因此应充分利用。
错误处理则用于优雅应对正式版本中可能发生的异常情况(无论概率多低)。这类情况可能包含可恢复问题(程序可继续运行)或不可恢复问题(程序必须终止,但至少能显示友好的错误信息并确保资源正确清理)。错误检测与处理既涉及运行时性能开销,也包含开发时间成本。
某些场景下处理方式并不明确。例如考虑以下函数:
double getInverse(double x)
{
return 1.0 / x;
}
若x为0.0,此函数将出现异常行为,我们需要防范这种情况。该采用断言还是错误处理?最佳方案或许是“两者兼用”。
调试过程中,若x为0.0时调用此函数,表明代码存在漏洞,我们需要立即察觉。因此断言显然是合适的选择。
然而在正式发布版本中,这种情况也可能合理发生(例如沿未测试的罕见路径)。若断言被编译掉且未设置错误处理,该函数将返回异常值并导致行为异常。此时更优解是检测异常并进行处理。
最终函数可能如下所示:
double getInverse(double x)
{
assert(x != 0.0);
if (x == 0.0)
// handle error somehow (e.g. throw an exception)
return 1.0 / x;
}
提示
鉴于此,我们建议如下:
- 使用断言检测编程错误、错误假设或正确代码中不应出现的条件。修复这些问题是程序员的责任,因此我们希望尽早发现它们。
- 使用错误处理应对程序正常运行期间可能出现的问题。
- 当某些情况不应发生但若发生时仍需优雅失败时,请同时使用这两种方法。
关于断言的限制与警告
断言存在若干陷阱与局限。首先,断言本身可能被错误编写。若出现此情况,断言要么在不存在错误时报告错误,要么在存在错误时未能报告。
其次,断言表达式应无副作用,因为当定义NDEBUG时断言表达式不会被求值(因此副作用不会生效)。否则,调试环境与发布环境(假设发布时禁用NDEBUG)的测试结果将不一致。
另需注意:abort()函数会立即终止程序,无法执行后续清理操作(如关闭文件或数据库)。因此断言仅适用于程序意外终止时数据损坏风险较低的场景。

浙公网安备 33010602011771号