如何使用C预处理器连接两次并扩展宏,如"arg ## _ ## MACRO"?

JJ.*_*JJ. 141 c concatenation token c-preprocessor

我正在尝试编写一个程序,其中一些函数的名称依赖于某个宏变量的值,宏如下:

#define VARIABLE 3
#define NAME(fun) fun ## _ ## VARIABLE

int NAME(some_function)(int a);
Run Code Online (Sandbox Code Playgroud)

不幸的是,宏NAME()将其转化为

int some_function_VARIABLE(int a);
Run Code Online (Sandbox Code Playgroud)

而不是

int some_function_3(int a);
Run Code Online (Sandbox Code Playgroud)

所以这显然是错误的方式.幸运的是,VARIABLE的不同可能值的数量很小所以我可以简单地做一个#if VARIABLE == n并单独列出所有情况,但我想知道是否有一个聪明的方法来做到这一点.

Jon*_*ler 208

标准C预处理器

$ cat xx.c
#define VARIABLE 3
#define PASTER(x,y) x ## _ ## y
#define EVALUATOR(x,y)  PASTER(x,y)
#define NAME(fun) EVALUATOR(fun, VARIABLE)

extern void NAME(mine)(char *x);
$ gcc -E xx.c
# 1 "xx.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "xx.c"





extern void mine_3(char *x);
$
Run Code Online (Sandbox Code Playgroud)

两个层次的间接

在对另一个答案的评论中,Cade Roux 为什么这需要两个间接层次.轻率的回答是因为这就是标准要求它起作用的方式; 你倾向于发现你需要与字符串化运算符相同的技巧.

C99标准的6.10.3节涵盖了"宏观替代",6.10.3.1涵盖了"论证替代".

在确定了调用类函数宏的参数之后,发生了参数替换.在替换列表中的参数,除非前面有一个###预处理记号或后跟一个##预处理标记(见下文),由对应的参数替换后其中所含的所有宏已经扩大.在被替换之前,每个参数的预处理标记都被完全宏替换,好像它们形成了预处理文件的其余部分; 没有其他预处理令牌可用.

在调用中NAME(mine),参数是'我的'; 它完全扩展到'我的'; 然后将其替换为替换字符串:

EVALUATOR(mine, VARIABLE)
Run Code Online (Sandbox Code Playgroud)

现在发现宏EVALUATOR,参数被隔离为'mine'和'VARIABLE'; 然后后者完全扩展为'3',并替换为替换字符串:

PASTER(mine, 3)
Run Code Online (Sandbox Code Playgroud)

其他规则(6.10.3.3'## operator')涵盖了此操作:

如果在类函数宏的替换列表中,参数紧跟在##预处理标记之前或之后,则该参数将被相应参数的预处理标记序列替换; [...]

对于类似对象和类似函数的宏调用,在重新检查替换列表以替换更多宏名称之前,##删除替换列表中的预处理标记的每个实例(不是来自参数),并且连接前面的预处理标记使用以下预处理标记.

因此,替换列表包含x后跟##,也##包括y; 所以我们有:

mine ## _ ## 3
Run Code Online (Sandbox Code Playgroud)

并且消除##令牌并连接两侧的令牌将'mine'与'_'和'3'组合在一起以产生:

mine_3
Run Code Online (Sandbox Code Playgroud)

这是期望的结果.


如果我们查看原始问题,代码是(适合使用'mine'而不是'some_function'):

#define VARIABLE 3
#define NAME(fun) fun ## _ ## VARIABLE

NAME(mine)
Run Code Online (Sandbox Code Playgroud)

NAME的论点显然是"我的",并且已经完全扩展.
遵循6.10.3.3的规则,我们发现:

mine ## _ ## VARIABLE
Run Code Online (Sandbox Code Playgroud)

##运营商被淘汰时,映射到:

mine_VARIABLE
Run Code Online (Sandbox Code Playgroud)

正如问题中所报道的那样.


传统的C预处理器

RobertRüger 问道:

对于没有令牌粘贴操作符的传统C预处理器,有没有办法做到这一点##

也许,也许不是 - 这取决于预处理器.标准预处理器的一个优点是它具有可靠的工作,而预标准预处理器则有不同的实现.一个要求是当预处理器替换注释时,它不会生成空间,因为ANSI预处理器需要这样做.GCC(6.3.0)C预处理器符合此要求; XCode 8.2.1的Clang预处理器没有.

当它工作时,这就完成了job(x-paste.c):

#define VARIABLE 3
#define PASTE2(x,y) x/**/y
#define EVALUATOR(x,y) PASTE2(PASTE2(x,_),y)
#define NAME(fun) EVALUATOR(fun,VARIABLE)

extern void NAME(mine)(char *x);
Run Code Online (Sandbox Code Playgroud)

请注意,在fun,和之间没有空格VARIABLE- 这很重要,因为如果存在,它会被复制到输出中,并且最终会mine_ 3得到名称,当然这在语法上并不合法.(现在,我可以把头发弄回来吗?)

使用GCC 6.3.0(运行cpp -traditional x-paste.c),我得到:

# 1 "x-paste.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "x-paste.c"





extern void mine_3(char *x);
Run Code Online (Sandbox Code Playgroud)

使用来自XCode 8.2.1的Clang,我得到:

# 1 "x-paste.c"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 329 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "x-paste.c" 2





extern void mine _ 3(char *x);
Run Code Online (Sandbox Code Playgroud)

那些空间破坏了一切.我注意到两个预处理器都是正确的; 不同的预标准预处理器展示了这两种行为,这使得在尝试移植代码时令牌粘贴成为一个非常烦人且不可靠的过程.带##符号的标准从根本上简化了这一点.

可能还有其他方法可以做到这一点.但是,这不起作用:

#define VARIABLE 3
#define PASTER(x,y) x/**/_/**/y
#define EVALUATOR(x,y) PASTER(x,y)
#define NAME(fun) EVALUATOR(fun,VARIABLE)

extern void NAME(mine)(char *x);
Run Code Online (Sandbox Code Playgroud)

GCC产生:

# 1 "x-paste.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "x-paste.c"





extern void mine_VARIABLE(char *x);
Run Code Online (Sandbox Code Playgroud)

关闭,但没有骰子.YMMV,当然,取决于您正在使用的预标准预处理器.坦率地说,如果你坚持使用不合作的预处理器,那么安排使用标准C预处理器代替预先标准的预处理器(通常有一种方法可以适当地配置编译器)可能更简单.花很多时间试图找到一种方法来完成这项工作.


Ste*_*non 32

#define VARIABLE 3
#define NAME2(fun,suffix) fun ## _ ## suffix
#define NAME1(fun,suffix) NAME2(fun,suffix)
#define NAME(fun) NAME1(fun,VARIABLE)

int NAME(some_function)(int a);
Run Code Online (Sandbox Code Playgroud)

老实说,你不想知道为什么会这样.如果你知道它为什么会起作用,那么你就会成为那个知道这类事情的人,每个人都会问你问题.=)

编辑:如果你真的想知道它为什么会起作用,我会高兴地发布一个解释,假设没有人打败我.

  • 请参阅Jonathan Leffler最精彩的解释. (6认同)

Cir*_*四事件 10

EVALUATOR两步模式的简单英语解释

我还没有完全理解 C 标准的每个单词,但我认为这是一个合理的工作模型,说明Jonathan Leffler 的答案中显示的解决方案如何工作,解释得更详细一些。如果我的理解不正确,请告诉我,希望有一个最小的例子可以打破我的理论。

出于我们的目的,我们可以将宏观扩张视为分三个步骤进行:

  1. (预扫描)宏参数被替换:
    • 如果它们是连接 ( A ## B) 或字符串化 ( #A) 的一部分,则它们将完全按照宏调用中给出的字符串进行替换,而不进行扩展
    • 否则,它们首先被完全扩展,然后才被替换
  2. 发生字符串化和串联
  3. 所有定义的宏都会展开,包括字符串化中生成的宏

没有间接的分步示例

主程序

#define CAT(x) pref_ ## x
#define Y a

CAT(Y)
Run Code Online (Sandbox Code Playgroud)

并将其扩展为:

gcc -E main.c
Run Code Online (Sandbox Code Playgroud)

我们得到:

pref_Y
Run Code Online (Sandbox Code Playgroud)

因为:

步骤1:Y是 的宏参数CAT

x出现在字符串化中pref_ ## x。因此,Y按原样粘贴,无需扩展:

pref_ ## Y
Run Code Online (Sandbox Code Playgroud)

第 2 步:发生串联,我们剩下:

pref_Y
Run Code Online (Sandbox Code Playgroud)

步骤 3:发生任何进一步的宏替换。但pref_Y不是任何已知的宏,因此将其单独保留。

我们可以通过实际添加一个定义来证实这个理论pref_Y

#define CAT(x) pref_ ## x
#define Y a
#define pref_Y asdf

CAT(Y)
Run Code Online (Sandbox Code Playgroud)

现在的结果是:

asdf
Run Code Online (Sandbox Code Playgroud)

因为上面的步骤 3pref_Y现在被定义为宏,因此会扩展。

间接的分步示例

但是,如果我们使用两步模式:

#define CAT2(x) pref_ ## x
#define CAT(x) CAT2(x)
#define Y a

CAT(Y)
Run Code Online (Sandbox Code Playgroud)

我们得到:

pref_a
Run Code Online (Sandbox Code Playgroud)

步骤1:CAT评估。

CAT(x)被定义为CAT2(x),因此定义中x的参数CAT不会出现在字符串化中:字符串化仅在CAT2扩展之后发生,这在这一步中看不到。

因此,Y是在被替换之前完全扩展的,经历步骤 1、2 和 3,我们在这里省略这些步骤,因为它很容易扩展为a。所以我们a投入CAT2(x)

CAT2(a)
Run Code Online (Sandbox Code Playgroud)

步骤 2:无需进行字符串化

步骤 3:展开所有现有宏。我们有了宏CAT2(a),所以我们继续扩展它。

x步骤3.1:的参数CAT2出现在字符串化中pref_ ## xa因此,按原样粘贴输入字符串,给出:

pref_ ## a
Run Code Online (Sandbox Code Playgroud)

步骤3.2:字符串化:

pref_a
Run Code Online (Sandbox Code Playgroud)

步骤 3:展开任何进一步的宏。pref_a不是任何宏,所以我们就完成了。

GCC 参数预扫描文档

GCC 关于此事的文档也值得一读:https ://gcc.gnu.org/onlinedocs/cpp/Argument-Prescan.html

奖励:这些规则如何防止嵌套调用变得无限

现在考虑:

#define f(x) (x + 1)

f(f(a))
Run Code Online (Sandbox Code Playgroud)

扩展到:

((a + 1) + 1)
Run Code Online (Sandbox Code Playgroud)

而不是无限。

让我们来分解一下:

f步骤1:使用参数调用外部x = f(a)

在 的定义中f,参数x不是 的定义中串联的(x + 1)一部分f。因此,在替换之前首先要完全扩展。

x = f(1)步骤1.1:我们根据步骤1、2和3充分展开论证,给出x = (a + 1)

现在回到步骤 1,我们采用完全扩展的x参数 equaling ,并将其放入Give(a + 1)的定义中:f

((a + 1) + 1)
Run Code Online (Sandbox Code Playgroud)

步骤 2 和 3:没有发生太多事情,因为我们没有字符串化,也没有更多的宏可以扩展。