【问题标题】:Why is assert a macro and not a function?为什么断言是宏而不是函数?
【发布时间】:2014-10-06 18:39:09
【问题描述】:

我的讲师在课堂上问过我这个问题,我想知道为什么它是宏而不是函数?

【问题讨论】:

  • 在实现我自己的断言时,我总是将其作为宏来执行,因为我希望能够从断言函数返回,而不仅仅是从断言函数返回。宏可以做某事并让调用函数返回 - 成功!
  • @Martin: ...和一个相当有问题的编程实践。隐藏跳转语句的宏?
  • @AndreyT:这确实值得怀疑。所以宏的名字不应该隐藏跳转。我以前用过这样的宏,如果名字好的话可以很清楚。
  • 据我所知,所有答案同样适用于 C++。
  • 我发现由GLib 定义的g_return 系列宏是那些非常明确命名的断言的一个很好的例子。

标签: c assert c-preprocessor


【解决方案1】:

简单的解释是标准要求assert 是一个宏,如果我们查看draft C99 standard(据我所知draft C11 standard 中的部分也是相同的) 部分7.2 诊断 段落2 说:

assert 宏应作为宏实现,而不是实际的 功能。如果宏定义被抑制以访问 实际功能,行为未定义。

为什么需要这个,Rationale for International Standard—Programming Languages—C 给出的理由是:

使 assert 成为真正的函数可能很困难或不可能,因此仅限于宏 表格。

这不是很丰富,但我们可以从其他要求中看出原因。回到7.21 部分说:

[...]如果 NDEBUG 在源文件中被定义为宏名 包含在哪里,断言宏被简单地定义为

#define assert(ignore) ((void)0)

根据NDEBUG的当前状态重新定义assert宏 每次都包含在内。

这很重要,因为它使我们能够以一种简单的方式在发布模式下关闭断言,在这种情况下您可能想要承担可能昂贵的检查成本。

第二个重要要求是需要使用宏__FILE____LINE____func__,这在7.2.1.1 部分中有介绍。断言宏 说:

[...] assert 宏写入有关特定调用的信息 失败的 [...] 后者分别是 预处理宏 __FILE_ _ 和 __LINE_ _ 和标识符 __func__) 以实现定义的格式在标准错误流上。165) 然后它调用中止函数。

脚注165 说:

所写的消息可能是以下形式:

Assertion failed: expression, function abc, file xyz, line nnn.

将其作为宏允许宏 __FILE__ 等在适当的位置进行评估,并且正如 Joachim 指出的宏允许它在消息中插入原始 表达式它会生成。

C++ 标准草案要求 cassert 标头的内容与标准 C 库中的 assert.h 标头的内容相同:

内容与标准C库头文件相同。

另请参阅:ISO C 7.2。

为什么 (void)0?

为什么使用(void)0 而不是其他什么都不做的表达式?我们可以想出几个原因,首先这是7.2.1.1部分中断言概要的样子:

void assert(scalar expression);

它说(强调我的):

assert 宏将诊断测试放入程序中;它扩展为一个空表达式。

表达式 (void)0 与需要以 void 表达式结尾的需求一致。

假设我们没有这个要求,其他可能的表达式可能会产生不良影响,例如允许在发布模式下使用 assert,而在调试模式下不允许使用,例如使用 plain 0 将允许我们使用 @987654345 @ 在分配中并且正确使用时可能会生成 expression result unused 警告。至于评论建议使用复合语句,我们可以从C multi-line macro: do/while(0) vs scope block 看到它们在某些情况下会产生不良影响。

【讨论】:

  • 我认为另一个重要原因是因为函数需要表达式具有特定类型,但该类型必须与任何其他类型兼容(从技术上讲,它必须与可能出现的任何类型兼容在if/while 声明中)
  • @Drew McGowen:在 C99 中,它只会有一个 _Bool 类型的参数作为“特定类型”。任何标量类型都可以隐式转换为_Bool
【解决方案2】:
  1. 它允许捕获文件(通过__FILE__)和行号(通过__LINE__
  2. 它允许assert 替换为在发布模式下构建时不执行任何操作的有效表达式(即((void)0)

【讨论】:

  • 准确地说,是((void)0)
  • 另外,它还允许在错误消息中将实际表达式表示为字符串。
  • @RobertAllanHenniganLeahy:他在说什么。是否断言(根据标准)不是由空字符串替换,而是在定义 NDEBUG 时替换为字符串((void)0)
  • assert 必须始终是有效的表达式。 “空字符串”不是一个有效的表达式,这就是为什么assert 不能被空字符串替换。我可以像这样使用asserta = (assert(b > 0), b)。这是完全有效的表达。如果assert突然变成一个空字符串,这个表达式就会失效。
  • 你们都误会了。 Joachim 的肉是 assert(1==2) 可以打印类似“断言失败:1==2”的消息。除非是宏,否则这是不可能的。
【解决方案3】:

如果在 include 时已经定义了一个名为 NDEBUG 的宏,则该宏将被禁用。这允许编码人员在调试程序时在源代码中包含尽可能多的断言调用,然后通过简单地包含如下行来禁用所有这些调用:

#define NDEBUG 

在其代码的开头,在包含<assert.h>之前。

因此,此宏旨在捕获编程错误,而不是用户或运行时错误,因为它通常在程序退出其调试阶段后被禁用。


将其作为函数会增加一些函数调用,并且您无法在发布模式下控制所有此类断言。

如果您使用函数,那么_FILE____LINE____func__ 将给出该断言函数代码的值。不是那个调用线路或调用函数的线路。

【讨论】:

  • 将其作为函数会增加一些函数调用,并且您无法在发布模式下控制所有此类断言。
【解决方案4】:

有些断言调用起来可能很昂贵。您刚刚编写了一个高性能矩阵求逆例程,并添加了健全性检查

assert(is_identity(matrix * inverse))

到最后。好吧,您的矩阵非常大,如果assert 是一个函数,那么在将其传递给断言之前需要花费大量时间进行计算。如果您不进行调试,您真的不想花时间。

或者断言可能相对便宜,但它包含在一个非常短的函数中,该函数将在内部循环中被调用。或其他类似情况。

通过将assert 改为宏,您可以在关闭断言时完全消除计算。

【讨论】:

  • Apple 的 swift 解决了在调用函数之前必须解析参数的问题,但毕竟它们没有预编译器。
  • @WorldSEnder 我有确切的想法。这是一个聪明的解决方案,他们甚至将它用于 &&、|| 等。自动关闭 FTW! :-p
【解决方案5】:

为什么断言是宏而不是函数?

因为它应该在DEBUG模式下编译,而不应该在RELEASE模式下编译。

【讨论】:

    猜你喜欢
    • 2013-11-15
    • 1970-01-01
    • 2018-08-22
    • 1970-01-01
    • 2015-02-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多