带有 2 个参数的 C 预处理器宏问题

Posted

技术标签:

【中文标题】带有 2 个参数的 C 预处理器宏问题【英文标题】:C Preprocessor Macro with 2 arguments issue 【发布时间】:2017-07-27 17:10:48 【问题描述】:

我的 C 代码中有这个宏:

#define ASSERT(ret, num) \  // make sure ret === num
        if (ret != num)  \
            fprintf(stderr, "Error [%d] at line [%d] in function [%s]. Date: [%s] Time: [%s]\n", \
                    ret, __LINE__, __FUNCTION__, __DATE__, __TIME__); \
            exit(ret); \
        

然后我这样称呼它(所有参数都是整数):

ASSERT(errNo, MPI_SUCCESS);
ASSERT(aliveNeighbors, 8);
ASSERT(newGrid, !NULL);

我收到类似 (GCC v5.4) 的错误:

expected identifier or ‘(’ before ‘if’
   if (ret != num)  \
error: stray ‘\’ in program
   ASSERT(errNo, MPI_SUCCESS);
error: stray ‘\’ in program
  ASSERT(aliveNeighbors, 8);
stray ‘\’ in program
  ASSERT(newGrid, !NULL);

这里有什么问题?

【问题讨论】:

删除\后的评论(// make sure ret === num) 请注意__DATE____TIME__ 是编译代码的日期和时间,而不是运行程序的日期和时间。它不太可能是有用的信息。 请注意,您不能在宏中使用// … <eol> cmets。你可以在里面使用/* … */ cmets,只要反斜杠继续在注释之后。当然,这些也可以延伸到多行(并且不需要在行尾使用反斜杠),但这也可能不是一个特别好的主意。您可以在宏的最后一行末尾使用// … <eol> @lurker:你根本不能在宏中使用// … <eol>。如果在行尾有\,则扩展的是注释,而不是宏体。 @RestlessC0bra:在ASSERT(ret, num) 成为无操作宏的意义上,“它有效”。反斜杠换行的行拼接发生在分析 cmets 之前,// … \<eol> 将注释继续到下一行;下一行末尾的任何\ 也会继续注释。 【参考方案1】:

给定宏定义:

#define ASSERT(ret, num) \  // make sure ret === num
        if (ret != num)  \
            fprintf(stderr, "Error [%d] at line [%d] in function [%s]. Date: [%s] Time: [%s]\n", \
                    ret, __LINE__, __FUNCTION__, __DATE__, __TIME__); \
            exit(ret); \
        

您有许多问题,其中许多问题都在您的错误消息中可见。

反斜杠换行符拼接发生在标准 (ISO/IEC 9899:2011) §5.1.1.2 翻译阶段中定义的处理的第 2 阶段:

    如有必要,物理源文件多字节字符以实​​现定义的方式映射到源字符集(为行尾指示符引入换行符)。三字母序列被相应的单字符内部表示替换。

    每个反斜杠字符 (\) 的实例都会被删除,紧跟一个换行符,将物理源代码行拼接成逻辑源代码行。只有任何物理源行上的最后一个反斜杠才有资格成为此类接头的一部分。非空源文件应以换行符结尾,在任何此类拼接发生之前不应紧跟反斜杠字符。

    源文件分解为预处理标记7)和空白字符序列(包括 cmets)。源文件不应以部分预处理标记或部分注释结尾。每个注释被一个空格字符替换。保留换行符。是否保留除换行符以外的每个非空空白字符序列还是由一个空格字符替换是由实现定义的。

    执行预处理指令,扩展宏调用,并执行_Pragma 一元运算符表达式。如果一个与通用字符名的语法匹配的字符序列是由token产生的 连接(6.10.3.3),行为未定义。 #include 预处理指令导致命名的头文件或源文件从阶段 1 到阶段 4 以递归方式进行处理。然后删除所有预处理指令。

(第 5-8 阶段完成了处理,但与此讨论无关。)

请注意,在阶段 3 中删除了 cmets,并且适当的预处理发生在阶段 4,对标记化的输入。

你的宏定义的第一行是:

    #define ASSERT(ret, num) \  // make sure ret === num

这将宏的主体定义为\。不能将 cmets 放在要继续宏的反斜杠之后,也不能在宏的主体中使用 // … <eol> 样式 cmets。如果您在注释后添加反斜杠,它将继续注释到下一行,并且宏体仍然是空的。您可以谨慎使用/* … */ cmets:

    #define ASSERT(ret, num)   /* make sure ret === num */ \

这将正常工作。因为原始宏有尾随 // … <eol> 注释,所以完成了宏。第一条错误信息是:

expected identifier or ‘(’ before ‘if’
   if (ret != num)  \

这是因为“宏定义”中的 if 行实际上不是宏的一部分,并且 if 在它出现的上下文中不是预期的。

其他三个错误基本相同:

error: stray ‘\’ in program
   ASSERT(errNo, MPI_SUCCESS);

宏扩展为仅一个反斜杠,但在有效的 C 程序的文本中不能有杂散的反斜杠,因此会出现错误消息。当反斜杠出现时,它们总是在带有特定其他字符(数字、某些字母、某些标点符号、换行符)的程式化上下文中。

宏的主体也有问题:

__DATE____TIME__ 是编译(预处理)代码的日期和时间,而不是运行代码的日期和时间。它们很少有用,在这种情况下也没有用。如果您需要程序运行时的日期和时间信息,您将定义并调用一个函数来确定和格式化当前日期/时间。 __FUNCTION__ 不标准; __func__ 是标准的预定义标识符。

正如所写,宏不能在某些情况下安全使用,例如:

if (alpha == omega)
    ASSERT(retcode, 0);
else if (gamma == delta)
    gastronomic_delight(retcode, gamma, alpha);

else 子句错误地与(半更正)ASSERT 中的 if 关联,而不是与 alpha == omega 测试关联。

将这些更改组合成一个可行的解决方案:

#define ASSERT(ret, num) \
            do  \
                if ((ret) != (num)) err_report(ret, __LINE__, __func__); \
             while (0)

你在哪里:

extern _Noreturn void err_report(int err, int line, const char *func);

此函数格式化消息并报告标准错误,确定时间和日期(如果合适)。它也会退出(因此_Noreturn 属性——C11 的一个特性。您可能更喜欢#include <stdnoreturn.h> 并使用noreturn 而不是_Noreturn。您可能会或可能不会决定添加__FILE__ 作为传递给的值函数;它需要另一个参数。

还要注意,调用:

ASSERT(newGrid, !NULL);

是可疑的。它转换为if ((newGrid) == (!0)),这不是您想要的。您必须更加努力地测试非空指针:

ASSERT(newGrid != NULL, 1);

这会将newGrid 中的值与NULL 进行比较,如果该值不为空,则生成1,如果为空,则生成0,并与1 参数进行比较。请注意,此测试的错误号不会有帮助 - 它将是 0

如果我错过了 cmets 中的任何关键点,请告诉我。

【讨论】:

非常感谢。如果你也能实现 err_report 那就太好了。 您可以在GitHub 上查看stderr.cstderr.h 中的代码。那里有一个err_report() 函数,但它的设计与我在这里设计的不同。但是,stderr.[ch] 中的代码涵盖了您可能需要的所有基础以及您不需要的少数基础。请注意,您的消息非常冗长。如果它们不经常出现,这并不重要,但如果它们变得频繁,可能会变得令人恼火。但是,这可能是另一天的问题。 (这些文件中的代码读起来并不简单。)

以上是关于带有 2 个参数的 C 预处理器宏问题的主要内容,如果未能解决你的问题,请参考以下文章

用引号定义C ++预处理器宏[重复]

Xcode:测试与调试预处理器宏

Doxygen C预处理器宏文档样式

用于返回重复一定次数的字符串的 C 预处理器宏

如何在 Rust 的 FFI 中使用 C 预处理器宏?

为啥预处理器宏是邪恶的,有啥替代方案?