当我在一个大型项目中工作时,我偶然发现了一个错误,在这个错误中,一个宏没有正确地扩展。结果输出是“EXPAND(0)
”,但EXPAND
被定义为“#define EXPAND(X)X
”,因此很明显输出应该是“0
”。
“没问题”,我心想。“这可能是一些愚蠢的错误,毕竟这里有一些讨厌的宏,有很多地方会出错”。正如我所想的那样,我将行为不端的宏隔离到它们自己的项目中,大约200行,并开始在MWE上工作以确定问题所在。200行变成了150,又变成了100,然后是20,10……令我震惊的是,这是我最后的MWE:
#define EXPAND(X) X
#define PARENTHESIS() ()
#define TEST() EXPAND(0)
EXPAND(TEST PARENTHESIS()) // EXPAND(0)
4行。
雪上加霜的是,几乎对宏的任何修改都能使它们正常工作:
#define EXPAND(X) X
#define PARENTHESIS() ()
#define TEST() EXPAND(0)
// Manually replaced PARENTHESIS()
EXPAND(TEST ()) // 0
#define EXPAND(X) X
#define PARENTHESIS() ()
#define TEST() EXPAND(0)
// Manually replaced TEST()
EXPAND(EXPAND(0)) // 0
// Set EXPAND to 0 instead of X
#define EXPAND(X) 0
#define PARENTHESIS() ()
#define TEST() EXPAND(0)
EXPAND(TEST PARENTHESIS()) // 0
但最重要也是最奇怪的是,下面的代码以完全相同的方式失败:
#define EXPAND(X) X
#define PARENTHESIS() ()
#define TEST() EXPAND(0)
EXPAND(EXPAND(EXPAND(EXPAND(TEST PARENTHESIS())))) // EXPAND(0)
这意味着预处理器完全能够扩展expand
,但是由于某种原因,它绝对拒绝在最后一步中再次扩展它。
现在,我将如何在我的实际程序中解决这个问题,这不是现在也不是现在。虽然解决方案是不错的(例如,将标记expand(TEST parenthese())
扩展到0
)的方法,但我最感兴趣的是:为什么?为什么C预处理器在第一种情况下得出结论“扩展(0)
”是正确的扩展,而在其他情况下却不是?
虽然很容易找到C预处理器的功能(以及您可以使用它的一些魔法),但我还没有找到一个解释它是如何实现的,我想借此机会更好地理解预处理器是如何完成它的工作的,以及它在扩展宏时使用什么规则。
那么,鉴于此:预处理器决定将最终宏扩展为“expand(0)
”而不是“0
”背后的原因是什么?
针对这种情况,宏替换有三个相关步骤:
在展开(测试括号())
:
展开
的参数、测试括号()
执行宏替换:
test
后面没有括号,因此它不被解释为宏调用。parenthese()
是一个宏调用,因此执行三个步骤:参数为空,因此不对它们进行处理。则括号()
将替换为()
。然后重新扫描()
并且找不到宏。expand(TEST())
。(test()
未重新扫描,因为它不是任何宏替换的结果。)测试()
替换为展开(0)
。展开(0)
,但取消显示展开
。在展开(TEST())
中:
expand
的参数执行宏替换:
test
的参数为空,因此不进行处理。测试()
替换为展开(0)
。0
替换expand(0)
。问题中的其他例子也类似。归结起来就是:
测试括号()
中,测试
后缺少括号,导致在处理封闭宏调用的参数时无法展开。括号
时,括号会放在它后面,但这是在扫描测试
之后,并且在处理参数期间不会重新扫描它。test
将被重新扫描并替换,但此时封闭宏的名称将被取消。宏扩展是一个复杂的过程,只有通过理解发生的步骤才能真正理解。
>
当识别具有参数的宏(宏名称标记后跟(
标记)时,扫描并拆分以下标记,直到匹配的)
(在,
标记上)。在此过程中不会发生宏扩展(因此、
s和)
必须直接存在于输入流中,而不能存在于其他宏中)。
每个宏参数,如果其名称出现在宏正文中,前面没有#
或##
或后面没有##
或后面没有##
,则“预存储”宏,以便宏展开--在替换到宏正文之前,所有参数中的宏都将被递归展开。
生成的宏参数令牌流被替换到宏的主体中。#
或##
参数中涉及的参数将基于步骤1中的原始解析器标记进行修改(字符串化或粘贴)和替换(对于这些参数,不执行步骤2)。
再次扫描生成的宏主体令牌流,以查找要展开的宏,但忽略当前正在展开的宏。此时,输入中的其他令牌(在步骤1中扫描和解析的内容之后)可以作为任何被识别的宏的一部分包括在内。
重要的是,有两个不同的递归展开(上面的步骤2和步骤4),只有步骤4中的一个忽略相同宏的递归宏展开。步骤2中的递归展开不会忽略当前宏,因此可以递归展开它。
对于上面的例子,让我们看看会发生什么。对于输入
EXPAND(TEST PARENTHESIS())
expand
并在参数列表测试括号()
中扫描x
测试
识别为宏(没有以下(
),但识别括号
:
()
插入到宏主体中,结果正好是:()
()
中的宏,但未找到任何宏展开(0)
test
。此时,expand
和test
都被抑制(由于处于步骤4扩展中),因此不会发生任何事情您的另一个示例expand(TEST())
不同
expand
被识别为宏,test()
被解析为参数x
expand
不会被取消
测试
被识别为具有空序列参数的宏展开(0)
test
并递归展开结果
expand
被识别为宏(请记住,此时只有test
被步骤4递归抑制--expand
在步骤2递归中,因此不被抑制),其参数为0
步骤2,扫描0
,但不会发生任何事情
- 步骤3,代入正文给出
0
- 步骤4,再次扫描
0
以查找宏(但再次未发生任何情况)
所以这里的最终结果是
0
我正在尝试实现一个正确的枚举到字符串宏,它将自动实现枚举值和代码中的名称之间的关联。 例如,我想定义一个名为“test”的新宏,如下所示: 这样,通过调用,我可以访问字符串。目前,这个的当前实现如下所示: 虽然当我这样做时,处理的枚举没有得到正确的名称分隔: 因为我希望使用而不是。 有没有办法用逗号分隔来展开名称? 干杯!
本文向大家介绍深入理解C预处理器,包括了深入理解C预处理器的使用技巧和注意事项,需要的朋友参考一下 C 预处理器不是编译器的组成部分,是编译过程中一个单独的步骤。C预处理器只是一个文本替换工具,它会指示编译器在实际编译之前完成所需的预处理。 所有的预处理器命令都是以井号(#)开头。它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。 下表包含所有重要的预处理器指令: 指令 描述 #
预处理器是一些指令,指示编译器在实际编译之前所需完成的预处理。 所有的预处理器指令都是以井号(#)开头,只有空格字符可以出现在预处理指令之前。预处理指令不是 C++ 语句,所以它们不会以分号(;)结尾。 我们已经看到,之前所有的实例中都有 #include 指令。这个宏用于把头文件包含到源文件中。 C++ 还支持很多预处理指令,比如 #include、#define、#if、#else、#line
#include <stdio.h> #define NAME "Joe" int main() { printf ("Hello %s\n", NAME); return 0; } 技巧 使用gcc -g编译生成的程序,是不包含预处理器宏信息的: (gdb) p NAME No symbol "NAME" in current context. 如果想在gdb中查看宏信息,可以使
本文向大家介绍C#中的预处理器指令详解,包括了C#中的预处理器指令详解的使用技巧和注意事项,需要的朋友参考一下 目录 1. #define 和 #undef 2. #if、#elif、#else 和#endif 3. #warning 和 #error 4. #region 和#endregion 5. #line 6. #pragma C#中有许多名为“预处理器指令”的命令。这些命令从来不会
主要内容:1. 预处理器示例,2. 预定义的宏,3. 预处理器运算符,4. 参数化宏Objective-C预处理器不是编译器的一部分,而是编译过程中的一个单独步骤。 简单来说,Objective-C预处理器只是一个文本替换工具,它指示编译器在实际编译之前进行必要的预处理。 我们将Objective-C预处理器称为OCPP。 所有预处理器命令都以井号()开头。它必须是第一个字符(前面不能有空格),并且为了便于阅读,预处理器指令应该从第一列开始。 以下部分列出了所有重要的预处理程序指