ModAssert,一个用于 C++ 的高级断言框架






4.79/5 (18投票s)
2006年2月6日
15分钟阅读

67986

775
一篇关于使用 ModAssert 的文章,这是一个包含 24 个断言的高级断言框架,可以使用富布尔值(Rich Booleans)。
- 下载源代码 (ModAssert + Rich Booleans) - 285 Kb
- 下载演示项目 - 34.6 Kb
- 从 SourceForge 下载 ModAssert (包含演示)
- 从 SourceForge 下载 Rich Booleans (ModAssert 需要)
要获取最新版本,请从 SourceForge 下载 ModAssert 和 Rich Booleans (前两个链接)。否则,请使用 CodeProject 上的下载,这些下载已剥离了 Linux 和 wxWidgets 支持 (后两个链接)。.
为什么选择 ModAssert
有很多断言包,那么 ModAssert 有什么不同呢?主要区别在于 **模块化**。其 128 个断言宏中有 96 个带有一个条件,它可以是一个求值为布尔值的表达式,也可以是 **富布尔值 (Rich Boolean)**。富布尔值是一个宏,它求值一个条件,并在条件失败时创建一个分析,然后将其传递给断言宏。断言宏然后显示和/或记录此分析以及其他有用的数据。一个简单的例子是富布尔值 rbEQUAL
,它检查其两个参数是否相等。如果不相等,它会创建一个包含两个参数值的分析。知道值比仅仅知道它们不同要有用得多。
这使得断言宏可以根据功能而变化,而不是自己进行分析。它有可以接受要显示的表达式、级别或组、可选操作或失败操作的断言宏。您可以组合使用这些。所以 ModAssert 没有像 ASSERT_EQUAL(a, b)
、ASSERT_LESS(a, b)
等这样的断言。取而代之的是,您将使用 MOD_ASSERT(rbEQUAL(a, b))
和 MOD_ASSERT(rbLESS(a, b))
。但您也可以轻松地编写 MOD_ASSERT_P(a<<b, rbEQUAL(a+b, c))
和 MOD_ASSERT_P(a<<b, rbLESS(a+b, c))
,它们会在断言失败时显示 a
和 b
的值。
其他断言包 | ModAssert |
ASSERT_EQUAL(a,b) |
MOD_ASSERT(rbEQUAL(a,b)) |
ASSERT_LESS(a,b) |
MOD_ASSERT(rbLESS(a,b)) |
ASSERT_MORE(a,b) |
MOD_ASSERT(rbMORE(a,b)) |
ASSERT_MSG_EQUAL("message", a,b) |
MOD_ASSERT_P("message", rbEQUAL(a,b)) |
ASSERT_MSG_LESS("message", a,b) |
MOD_ASSERT_P("message", rbLESS(a,b)) |
ASSERT_MSG_MORE("message", a,b) |
MOD_ASSERT_P("message", rbMORE(a,b)) |
ASSERT_LEVEL_EQUAL(Warning, a,b) |
MOD_ASSERT_G(Warning, rbEQUAL(a,b)) |
ASSERT_LEVEL_LESS(Warning, a,b) |
MOD_ASSERT_G(Warning, rbLESS(a,b)) |
ASSERT_LEVEL_MORE(Warning, a,b) |
MOD_ASSERT_G(Warning, rbMORE(a,b)) |
实际上,这只显示了 ModAssert 功能的一小部分。
有近 60 个富布尔值,以及 96 个模块化断言,这意味着近 6000 种组合。而有些富布尔值本身也是模块化的。例如,富布尔值 rbSTRING
比较两个字符串。它的第二个参数是一个运算符 (==, <, <=, >, >= 或 !=),第四个参数是一个指定比较方式的对象 (区分大小写或不区分,是否使用区域设置,字符串类型),因此它有两个模块化级别。富布尔值 rbIN_RANGE
、rbIN_ARRAY
、rbIN_CONTAINER
和 rbIN_XCONTAINER
在范围、数组或容器上工作,并以最后一个参数作为测试方式的对象。例如,它可以检查范围是否已排序,或严格排序,这为富布尔值增加了一个模块化级别。或者它可以检查所有元素、至少一个元素或恰好一个元素是否满足给定条件。因此,这里富布尔值有两个模块化级别。还有类似的函数在两个范围、数组、容器或组合上工作。所以实际上组合的数量远远超过 6000 种。
ModAssert 具有更多功能,可让您实现几乎所有您想要的断言功能。
- 添加表达式和消息以显示在错误消息旁边。
- 为断言分配级别或组,并决定显示哪些级别和组。
- 允许用户选择是否采取可选操作 (例如,在断言反复失败时退出循环)。
- 允许用户选择是否不再显示某个断言,或文件中的断言,或所有断言。
- 在显示断言后执行一个操作 (例如,抛出异常),即使断言被禁用。
- 创建自定义断言显示器和记录器,或使用提供的。
- 在断言失败时向每个显示器和记录器提供额外信息,例如时间、线程 ID、当前目录...;您可以添加自己的信息。
- 线程安全。
- 禁用报告以减小可执行文件大小。这可以按程序、源文件、级别或组进行。
- 使用五种不同的编译器 (Visual C++ 6.0、2003 和 2005,以及 Windows 和 Linux 上的 gcc) 和大多数警告级别进行了测试。提供了项目文件和配置文件。
背景
我编写 ModAssert 是因为我不喜欢不区分断言和它们测试的条件断言,例如 ASSERT_EQUAL(a,b)
。如果您想要添加消息、在禁用断言时求值其参数、抛出异常或检查 a 是否小于 b 的断言,您最终会得到大量的组合。为了解决这个问题,ModAssert 中的断言可以接受富布尔值作为其参数,就像布尔条件一样。富布尔值会执行分析,并在条件失败时保存要显示的调试信息。一个例子是 MOD_ASSERT(rbEQUAL(a,b))
。在这里,rbEQUAL(a,b)
是富布尔值,但它也可以是 rbLESS(a,b)
、rbEQUAL_TYPES(a,b)
、rbEQUAL_BITWISE(a,b)
或近六十种其他富布尔值之一。
此外,大多数断言框架不如 ModAssert 灵活或可扩展。
使用代码
入门
首先,在您需要的配置中构建 Rich Booleans 库和 ModAssert 库。项目文件已为此提供。
最好使用环境变量指向 Rich Booleans 和 ModAssert 安装的根目录 (例如:c:/modassert-1.3)。这样可以更轻松地迁移和升级。演示和下面的文本使用 RICHBOOL
和 MODASSERT
,因此最好使用这些名称。
在您的项目中,将 $(RICHBOOL)/include 和 $(MODASSERT)/include 添加到您的 include 路径,并将 $(RICHBOOL)/<configuration> 和 $(MODASSERT)/<configuration> 添加到您的库 include 路径,其中 <configuration> 是您的配置的子目录 (例如,DebugMTD)。最后,将您的应用程序链接到 richbool.lib 和 modassert.lib。
在 POSIX 系统上,使用 makefiles。对 Rich Booleans 和 ModAssert 都输入 ./configure,然后是 make 和 make install。这将把头文件和库安装到编译器已经查找的地方。
在主目录的 Console、Win32 或 wxgui 子目录中,以与 Rich Booleans 和 ModAssert 库相同的配置构建库,并链接到它。分别通过调用 ModAssert::SetupForConsole()
(用于控制台应用程序)、ModAssert::SetupForWin32(hInstance)
(用于 Win32 应用程序) 或 ModAssert::SetupForWxWidgets()
(用于 WxWidgets 应用程序) 来激活这些文件中的代码,以便显示断言。控制台版本将有关失败断言的信息写入标准输出,并在标准输入中询问要采取的操作。Win32 和 WxWidgets 版本显示一个包含所有信息的对话框,并请求采取操作,并将相同的信息跟踪到调试窗口,以便您可以稍后查看。
上一步的替代方法是编写自己的显示器和记录器。无论哪种情况,您都可以添加其他记录器,尤其是文件记录器。
在您要使用 ModAssert 的源文件中包含 "modassert/assert.hpp"。
8 个基本断言宏
有 8 个基本断言宏,可以扩展。它们是 MOD_ASSERT
、MOD_VERIFY
、MOD_CHECK
、MOD_FAIL
、MOD_CHECK_FAIL
、MOD_VERIFY_V
、MOD_CHECK_V
和 MOD_CHECK_B
。
MOD_ASSERT
和 MOD_VERIFY
有一个参数,即条件。这可以是一个布尔表达式或一个富布尔值。富布尔值是首选,因为它提供更多信息。它们之间的区别是:如果定义了 NDEBUG (例如,在 Visual Studio 的 Release 模式下),MOD_ASSERT
会被预处理器完全删除,而 MOD_VERIFY
仍然会对其参数进行求值 (但不报告条件失败)。这两个宏用于 **意外错误**,即代码错误的后果。
示例
#include "modassert/assert.hpp"
...
MOD_ASSERT(rbEQUAL(a,b));
如果条件失败,这将在 Win32 应用程序中显示以下对话框 (值可能不同,当然)
您可以看到,分析有很大的空间,这里几乎没有用到,但其他分析内容更长。
MOD_CHECK
有第二个参数,即条件失败时应执行的操作。这用于所谓的 **预期错误**,因为它们不是代码错误的后果,而是用户或其他来源的不正确操作,例如,用户输入了无效值,或者文件是只读的。如果定义了 NDEBUG,条件仍然会被求值,如果条件失败,操作仍然会执行。任何时候您想要错误处理,都应该使用 MOD_CHECK
宏,以便所有关于它的信息以相同的方式被显示和/或记录。
示例
MOD_CHECK(rbLESS(n, 100), throw MyException());
MOD_FAIL
没有参数,等同于 MOD_ASSERT(false)
。它应该用在您期望应用程序无法到达的地方。
示例
for (int i=0; i<n; ++i)
{
if (a[i]>10)
return a[i];
}
MOD_FAIL; // at least one should be bigger than 10
MOD_CHECK_FAIL
有一个参数,即失败操作,等同于 MOD_CHECK(false, action)
。它应该用在您的应用程序不应该到达的地方,除非发生了一些预期错误 (如 MOD_CHECK
)。
示例
for (int i=0; i<n; ++i)
{
if (a[i]>10)
return a[i];
}
MOD_CHECK_FAIL(throw MyException()); // at least one should be bigger than 10
MOD_VERIFY_V
类似于 MOD_VERIFY
,但返回一个值。如果您不使用富布尔值,则返回的值是条件。这对于返回非 NULL 指针的函数非常有用。如果您使用富布尔值,它将返回富布尔值的一个参数,通常是第一个参数。
示例
Widget *widget = MOD_VERIFY_V(CreateWidget());
MOD_CHECK_V
类似于 MOD_CHECK
,但返回一个值。如果您不使用富布尔值,则返回的值是条件。这对于返回非 NULL 指针的函数非常有用。如果您使用富布尔值,它将返回富布尔值的一个参数,通常是第一个参数。与 MOD_CHECK
不同,失败操作应该是一个可以对其调用 operator()()
的表达式。
MOD_CHECK_B
类似于 MOD_CHECK
,但没有失败操作,并返回一个 ModAssert::UseBool
对象。这是一个类,它的对象可以转换为布尔值,因此您可以将其用作 if 语句的条件。如果您不将其转换为布尔值,则断言将在其析构函数中失败。它具有转移语义,因此您可以将其用作函数的返回值,并将值的检查留给函数调用者。
示例
if (!MOD_CHECK_B(a==10))
{
...
}
注意:如果您在 MOD_VERIFY_V
、MOD_CHECK_V
或 MOD_CHECK_B
中使用富布尔值,您应该使用另一种类型的富布尔值,即以 rbv 开头的,而不是 rb。
扩展基本宏
可以通过添加指定您可提供哪些额外参数的后缀来扩展基本宏。如果您添加一个或多个后缀,它们应该以一个下划线开头。额外参数总是在条件之前 (与 MOD_CHECK
的失败操作不同)。
后缀 **P** 允许您 **添加表达式和消息**。您可以通过用 **<<** 分隔来提供多个。如果您使用 MOD_VERIFY_V
、MOD_CHECK_V
或 MOD_CHECK_B
,则不应使用 << 分隔参数,而应使用逗号,并用括号括起来。
示例
MOD_ASSERT_P(a << b << c << d, rbEQUAL(a+b, c+d));
int sum = MOD_VERIFY_VP((a, b, c, d), rbEQUAL(a+b, c+d));
在此示例中,将显示 a+b
和 c+d
的值,因为它们是富布尔值的参数,但也会显示 a
、b
、c
和 d
。
如果条件失败,这将在 Win32 应用程序中显示以下对话框 (值可能不同,当然)
您可以混合消息和参数。当一个参数是字面字符串时 (即,它在 " 之间),ModAssert 会识别它。
示例
MOD_ASSERT_P("number of spaces and tabs should be less than the string length"
<< nrSpaces << nrTabs,
rbLESS(nrSpaces+nrTabs, str.size()));
后缀 **G** 允许您 **添加组或级别**。组是 ModAssert::Group<ModAssert::ReportFailure>
、ModAssert::Group<ModAssert::ReportAlways>
或 ModAssert::Group<ModAssert::ReportNone>
类型的对象,分别用于在断言失败时、始终或从不显示和记录该断言。如果您在多个断言中使用这样的组,您可以通过更改组的类型来打开或关闭它们。如果您在调试时需要更多信息,甚至可以将其更改为 ModAssert::Group<ModAssert::ReportAlways>
,但不要忘记在不再需要时将其改回。如果您想对没有组的断言执行此操作,您可以使用预定义的 ModAssert::IfSuccess
对象。组可以与 **||** 和 **&&** 组合。
级别可以是 ModAssert::Info
、ModAssert::Warning
、ModAssert::Error
或 ModAssert::Fatal
类型。可以通过 **%** 将级别添加到组或组的组合中,但只能添加一个级别。添加级别很有用,因为您可以按级别控制断言的显示和记录。默认情况下,断言的级别是 ModAssert::Error
(正如您在上面的对话框中看到的)。
示例
ModAssert::Group<ModAssert::ReportAlways> group;
MOD_ASSERT_G(group % ModAssert::Warning, rbLESS(foo(a), foo(b)));
后缀 **O** (大写 o,不是数字 0) 允许您 **添加可选操作**。这需要两个额外的参数。第一个是操作,第二个是向用户显示的可选操作的描述。这有助于让用户从大量断言失败的长循环中脱身 (另一种选择是在 "停止显示断言" 框中选择 "此断言",其区别在于有问题的代码的执行将继续)。操作是代码本身,除非您使用 MOD_VERIFY_V
、MOD_CHECK_V
或 MOD_CHECK_B
,然后它应该是一个可以调用 operator()()
的表达式,例如,一个不接受参数的函数或一个具有 operator()()
的类对象。
示例
for (int i=0; i<10000; ++i)
{
...
MOD_ASSERT_O(return false, "Stop processing", rbLESS(a, b));
}
您还可以组合这些后缀,顺序为 P、G、O。它们的参数顺序相同。
示例
MOD_ASSERT_PGO(a<<b, ModAssert::Fatal, return false,
"Stop processing", rbLESS(foo(a), foo(b)));
这为您提供了总共 64 个断言宏。实际上,还有 64 个与默认参数有关 (请参阅文档),所以总数实际上是 128 个。其中 96 个可以将富布尔值作为条件,近 60 个。这为您提供了近 6000 种组合。没有其他断言包提供此功能。大多数断言包甚至没有 128 个宏,即使包括执行分析的宏,例如 ASSERT_EQUAL(a,b)
。
添加信息
您可以通过派生一个类 InfoProviders::InfoProvider
,创建一个您的类的对象,并将其传递给 InfoProviders::AddInfoProvider
来添加额外信息到显示器和记录器。 InfoProviders::InfoProvider
有两个纯虚方法 std::string GetType()
和 std::string GetInfo(bool success, const ModAssert::Context& context, const ModAssert::GroupList* groupList)
。第一个应该说明它提供的信息类型 (例如,“线程 ID”),第二个应该提供信息。您通过这种方式添加的信息提供者会被提供的记录器和显示器调用。如果您创建自己的记录器和显示器,它们也应该这样做。
如果您使用 ModAssert::SetWin32Handler
,许多这样的信息提供者会被自动添加,例如,一个带有线程 ID 的,一个带有时间和日期的,以及一个带有 GetLastError()
返回值的 (它甚至会添加相应的文本)。在上面的对话框中,您可以看到线程 ID,但看不到 GetLastError()
的返回值,因为它只在不为零时显示。
何时报告断言
报告是指记录和显示。默认情况下,只有在未定义符号 NDEBUG 时才会报告断言。所以,例如,使用 MS Visual Studio,它们会在 Debug 模式下报告,但在 Release 模式下不会。当断言报告被禁用时,可执行文件的大小会减小,因为 MOD_ASSERT
和 MOD_FAIL
宏会被完全删除,而 MOD_VERIFY
、MOD_CHECK
和 MOD_CHECK_FAIL
会被简化。
但是,通过全局定义符号 MOD_ASSERT_REPORT
,您可以在定义 NDEBUG 时打开断言的报告。如果您想在客户现场记录应用程序中的错误,这会很有用。在未定义 NDEBUG 时定义 MOD_ASSERT_DONT_REPORT
会禁用断言的报告。
这两个符号可以通过在包含 modassert/assert.hpp 之前定义 MOD_ASSERT_REPORT_FILE
和 MOD_ASSERT_DONT_REPORT_FILE
来按源文件覆盖。
富布尔值 (Rich Booleans)
上面的示例展示了一些富布尔值。讨论近 60 个富布尔值会太长,但我会给出一个可用富布尔值的概述。有关完整描述,请参阅 Rich Booleans 包的手册。
- 两个对象之间的关系:rbEQUAL、rbLESS、...
- 按位比较:rbEQUAL_BITWISE、rbBITS_ON、rbBITS_OFF、...
- 类型检查 (带 RTTI):rbEQUAL_TYPES、rbHAS_TYPE、...
- 对范围和容器进行操作:rbIN_RANGE、rbIN_RANGES、rbIN_CONTAINER、rbIN_CONTAINERS、... 这些有一个额外的参数,用于指定要执行的检查类型,例如 Sorted、Compare、Has、Unique、...
- 字符串比较:rbSTRING、rbSTRING_BEGINS_WITH、rbSTRING_ENDS_WITH、rbSTRING_CONTAINS。这些有一个额外的参数,用于指定要执行的检查类型,例如是否区分大小写。
- 逻辑表达式:rbAND、rbOR、rbXOR。这些可以有富布尔值或普通布尔表达式作为参数。
- 异常:rbEXCEPTION 以异常作为参数,并使用其中的信息。
关注点
通常,断言宏 (和其他语句宏) 是使用 'do { ... } while (false)
' 编写的。但是,因为开发者可以为某些断言宏提供操作作为参数,这也可以是 break
或 continue
,这显然行不通。因此,我必须使用类似的技巧,即 'if { ... } else 0
'。您也可以 (实际上应该) 在其后加上分号,而 0 是一个合法的 C++ 命令,它什么也不做。我在 if
和 if-else
中测试过,不带花括号,效果很好。
所以您可以编写以下代码
for (int i=0; i<10000; ++i)
{
... // do some processing
int a = foo(i);
MOD_ASSERT_O(break, "Stop processing", rbLESS(a, 10));
}
我还了解到,操作可以很长,只要所有逗号都在括号内,例如:
MOD_ASSERT_O(
++p; // semicolon is allowed here
if (b)
{
int n=0;
foo(n, 1); // fine, comma is between parentheses
},
"call foo if b",
rbMORE(a, 0)
);
这里,宏 MOD_ASSERT_O
只有三个参数:操作、描述和富布尔值。
一个有趣的问题是 GetLastError()
总是返回 0,即使我确信它不会。我发现日志记录到文件会将 GetLastError()
的返回值重置为 0。因此,我添加了钩子系统 (请参阅下载中的文档),这些钩子会在 ModAssert 调用记录器之前被调用。然后我创建了一个钩子来存储 GetLastError()
的返回值,该返回值稍后由 InfoProvider
使用。
许可证
ModAssert 包和 Rich Booleans 包都在 wxWindows 库许可证下分发。这基本上是 LGPL,但增加了一个例外,您可以在不公开源代码的情况下使用该库。您甚至不需要提及您使用了 ModAssert 或 Rich Booleans。有关此许可证的更多信息,请参阅 https://open-source.org.cn/licenses/wxwindows.php。
更多信息
本文并未描述 ModAssert 的所有功能。有关所有功能的描述,请参阅其文档。