65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (18投票s)

2006年2月6日

15分钟阅读

viewsIcon

67986

downloadIcon

775

一篇关于使用 ModAssert 的文章,这是一个包含 24 个断言的高级断言框架,可以使用富布尔值(Rich Booleans)。

要获取最新版本,请从 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)),它们会在断言失败时显示 ab 的值。

其他断言包 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 用 3 个宏实现其他断言包需要 9 个宏的功能。
实际上,这只显示了 ModAssert 功能的一小部分。

有近 60 个富布尔值,以及 96 个模块化断言,这意味着近 6000 种组合。而有些富布尔值本身也是模块化的。例如,富布尔值 rbSTRING 比较两个字符串。它的第二个参数是一个运算符 (==, <, <=, >, >= 或 !=),第四个参数是一个指定比较方式的对象 (区分大小写或不区分,是否使用区域设置,字符串类型),因此它有两个模块化级别。富布尔值 rbIN_RANGErbIN_ARRAYrbIN_CONTAINERrbIN_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.libmodassert.lib

在 POSIX 系统上,使用 makefiles。对 Rich Booleans 和 ModAssert 都输入 ./configure,然后是 make 和 make install。这将把头文件和库安装到编译器已经查找的地方。

在主目录的 ConsoleWin32wxgui 子目录中,以与 Rich Booleans 和 ModAssert 库相同的配置构建库,并链接到它。分别通过调用 ModAssert::SetupForConsole() (用于控制台应用程序)、ModAssert::SetupForWin32(hInstance) (用于 Win32 应用程序) 或 ModAssert::SetupForWxWidgets() (用于 WxWidgets 应用程序) 来激活这些文件中的代码,以便显示断言。控制台版本将有关失败断言的信息写入标准输出,并在标准输入中询问要采取的操作。Win32 和 WxWidgets 版本显示一个包含所有信息的对话框,并请求采取操作,并将相同的信息跟踪到调试窗口,以便您可以稍后查看。

上一步的替代方法是编写自己的显示器和记录器。无论哪种情况,您都可以添加其他记录器,尤其是文件记录器。

在您要使用 ModAssert 的源文件中包含 "modassert/assert.hpp"。

8 个基本断言宏

有 8 个基本断言宏,可以扩展。它们是 MOD_ASSERTMOD_VERIFYMOD_CHECKMOD_FAILMOD_CHECK_FAILMOD_VERIFY_VMOD_CHECK_VMOD_CHECK_B

MOD_ASSERTMOD_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_VMOD_CHECK_VMOD_CHECK_B 中使用富布尔值,您应该使用另一种类型的富布尔值,即以 rbv 开头的,而不是 rb。

扩展基本宏

可以通过添加指定您可提供哪些额外参数的后缀来扩展基本宏。如果您添加一个或多个后缀,它们应该以一个下划线开头。额外参数总是在条件之前 (与 MOD_CHECK 的失败操作不同)。

后缀 **P** 允许您 **添加表达式和消息**。您可以通过用 **<<** 分隔来提供多个。如果您使用 MOD_VERIFY_VMOD_CHECK_VMOD_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+bc+d 的值,因为它们是富布尔值的参数,但也会显示 abcd

如果条件失败,这将在 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::InfoModAssert::WarningModAssert::ErrorModAssert::Fatal 类型。可以通过 **%** 将级别添加到组或组的组合中,但只能添加一个级别。添加级别很有用,因为您可以按级别控制断言的显示和记录。默认情况下,断言的级别是 ModAssert::Error (正如您在上面的对话框中看到的)。

示例

ModAssert::Group<ModAssert::ReportAlways> group;
MOD_ASSERT_G(group % ModAssert::Warning, rbLESS(foo(a), foo(b)));

后缀 **O** (大写 o,不是数字 0) 允许您 **添加可选操作**。这需要两个额外的参数。第一个是操作,第二个是向用户显示的可选操作的描述。这有助于让用户从大量断言失败的长循环中脱身 (另一种选择是在 "停止显示断言" 框中选择 "此断言",其区别在于有问题的代码的执行将继续)。操作是代码本身,除非您使用 MOD_VERIFY_VMOD_CHECK_VMOD_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_ASSERTMOD_FAIL 宏会被完全删除,而 MOD_VERIFYMOD_CHECKMOD_CHECK_FAIL 会被简化。

但是,通过全局定义符号 MOD_ASSERT_REPORT,您可以在定义 NDEBUG 时打开断言的报告。如果您想在客户现场记录应用程序中的错误,这会很有用。在未定义 NDEBUG 时定义 MOD_ASSERT_DONT_REPORT 会禁用断言的报告。

这两个符号可以通过在包含 modassert/assert.hpp 之前定义 MOD_ASSERT_REPORT_FILEMOD_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)' 编写的。但是,因为开发者可以为某些断言宏提供操作作为参数,这也可以是 breakcontinue,这显然行不通。因此,我必须使用类似的技巧,即 'if { ... } else 0'。您也可以 (实际上应该) 在其后加上分号,而 0 是一个合法的 C++ 命令,它什么也不做。我在 ifif-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 的所有功能。有关所有功能的描述,请参阅其文档。

© . All rights reserved.