如何使用C预处理器连续两次合并并展开类似于"arg ## _ ## MACRO"的宏?

181

我正在尝试编写一个程序,在这个程序中,某些函数的名称取决于一个特定宏变量的值,使用如下宏定义:

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

int NAME(some_function)(int a);

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

int some_function_VARIABLE(int a);

而不是

int some_function_3(int a);

这显然不是正确的做法。幸运的是,VARIABLE的可能值很少,所以我可以简单地使用#if VARIABLE == n并单独列出所有情况,但有没有更聪明的方法呢?


3
你确定不想使用函数指针吗? - György Andrasek
9
函数指针在运行时起作用,预处理器在编译时(之前)起作用。尽管两者都可用于相同的任务,但它们之间存在差异。 - Chris Lutz
1
关键是它用于一个快速的计算几何库...该库已经为某个特定维度进行了硬编码。然而,有时候需要使用几个不同的维度(比如说2和3),因此需要一种简单的方法来生成依赖于维度的函数和类型名称的代码。另外,这段代码是用 ANSI C 编写的,因此在这里不能使用带有模板和特化的花哨 C++ 代码。 - JJ.
2
投票重新开放,因为这个问题具体涉及递归宏展开和https://dev59.com/CnVC5IYBdhLWcg3wrDNd是一个通用的“它有什么好处”。这个问题的标题应该更加精确。 - Ciro Santilli OurBigBook.com
我希望这个例子能够被简化:在#define A 0 \n #define M a ## A中发生了相同的情况,拥有两个##并不是关键。 - Ciro Santilli OurBigBook.com
3个回答

254

标准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);
$

两级间接寻址

在回答另一个问题的评论中,Cade Roux 问道为什么需要两个级别的间接寻址。简单回答是因为标准要求如此;你通常也会发现在字符串化操作符上也需要等效的技巧。

C99标准的第6.10.3节涵盖了“宏替换”,6.10.3.1涵盖了“参数替换”。

在确认宏类函数调用的实参后,进行参数替换。除非在替换列表中的参数前面有一个###预处理标记,或者在其后有一个##预处理标记(见下文),否则将被相应的参数替换。在所有包含其中的宏扩展之后,每个参数的预处理标记都像它们是预处理文件的其余部分一样被完全宏替换;其他预处理标记不可用。

在调用NAME(mine)中,实参为“mine”;它被完全展开为“mine”;然后被替换到替换字符串中:

EVALUATOR(mine, VARIABLE)

现在发现了宏EVALUATOR,并将参数分离为“mine”和“VARIABLE”;然后将后者完全展开为“3”,并替换到替换字符串中:

PASTER(mine, 3)
该操作受其他规则的覆盖(6.10.3.3“##运算符”):
如果在函数宏的替换列表中,参数紧接着前面或后面有一个##预处理标记,则该参数将被相应参数的预处理标记序列所替换;对于对象宏和函数宏调用,重复检查替换列表之前,在替换列表中(而不是来自参数)的每个##预处理标记实例都将被删除,并将前一个预处理标记连接到后一个预处理标记。因此,替换列表包含x,后面跟着##,还有##后面跟着y;我们有:
mine ## _ ## 3

消除##令牌并将两侧的令牌连接起来,将'mine'与'_'和'3'组合起来,得到:

mine_3

这是期望的结果。


如果我们看一下原始问题,代码如下(为了使用“mine”而不是“some_function”进行了调整):

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

NAME(mine)

NAME 的参数明显是'mine',已经完全扩展了。
按照 6.10.3.3 的规则,我们得到:

mine ## _ ## VARIABLE

当省略##操作符后,其映射为:

mine_VARIABLE

完全按照问题中所报告的。


传统C预处理器

Robert Rüger 问道:

在没有具有标记粘贴运算符##的传统C预处理器中,有没有任何方法可以做到这一点?

也许有,也许没有——这取决于预处理器。标准预处理器的一个优点是它具有这个可靠工作的设施,而对于预标准预处理器有不同的实现。其中一个要求是当预处理器替换注释时,它不会生成空格,因为ANSI预处理器要求这样做。GCC(6.3.0)C预处理器满足这个要求;XCode 8.2.1的Clang预处理器则不满足。

当它工作时,它完成以下工作(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);
请注意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);

使用 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);

这些空格会破坏一切。我注意到两个预处理器都是正确的;不同的预先标准处理器展示了这两种行为,这使得当尝试移植代码时,记号粘贴变得极其麻烦和不可靠。采用带有##符号的标准大大简化了这一过程。

可能还有其他方法来做到这一点。但是,这种方法行不通:

#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 生成:

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





extern void mine_VARIABLE(char *x);

接近成功,但还差一点。当然,这取决于您使用的预标准预处理器。如果您被卡在一个不合作的预处理器中,实际上使用标准C预处理器来代替预先标准化的预处理器可能会更简单(通常有一种配置编译器的方法),而不是花费大量时间尝试解决问题。


1
是的,这个解决了问题。我知道双重递归的技巧——至少要尝试一次字符串化——但不知道如何做到这一点。 - JJ.
有没有办法使用传统的C预处理器来完成这个任务,而不使用标记粘贴运算符##? - Robert Rüger
1
@RobertRüger:它会使答案的长度加倍,但我已经添加了信息来覆盖cpp -traditional。请注意,这并没有一个明确的答案——它取决于您所拥有的预处理器。 - Jonathan Leffler
非常感谢您的回答。这太棒了!与此同时,我还找到了另一种略有不同的解决方案。请参见此处。它也存在一个问题,即不能与clang一起使用。幸运的是,这对我的应用程序不是问题... - Robert Rüger

35

使用:

#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);

说实话,你其实不需要知道这个为什么能够起作用。如果你知道了,你就会成为工作中懂这种事情的那个人,其他人都会来问你问题。


你能解释一下为什么需要两个间接级别吗?我有一个带有一个间接级别的答案,但是我删除了这个答案,因为我必须在我的Visual Studio中安装C++,然后它就无法工作了。 - Cade Roux
我想成为工作中懂这种事情的那个人。 :) - undefined

8
“EVALUATOR” 两步模式的通俗解释

虽然我没有完全理解 C 标准中的每一个词,但我认为这是一个合理的工作模型,用更详细的方式解释了 Jonathan Leffler 的答案 中展示的解决方案。如果我的理解有误,请让我知道,最好提供一个破坏了我的理论的最小示例。

对于我们的目的,我们可以将宏展开视为三个步骤:

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

没有间接引用的逐步示例

main.c

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

CAT(Y)

并用以下方式扩展:

gcc -E main.c

我们得到:

pref_Y

因为:

步骤1:YCAT的宏参数。

x出现在字符串化pref_ ## x中。因此,Y作为原样粘贴而没有扩展,得到:

pref_ ## Y

第二步:进行串联,最终得到:

pref_Y

第三步:进行任何进一步的宏替换。但是pref_Y不是任何已知的宏,因此它被保留不变。
我们可以通过向pref_Y添加定义来确认这个理论:
#define CAT(x) pref_ ## x
#define Y a
#define pref_Y asdf

CAT(Y)

现在的结果将会是:

asdf

因为在上述第三步中,pref_Y 现在被定义为宏,因此会被展开。

带有间接性的逐步示例

然而,如果我们使用两步模式:

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

CAT(Y)

我们得到:

pref_a

步骤1:计算CAT

CAT(x)被定义为CAT2(x),所以在定义时参数x不出现在字符串化中:字符串化只发生在CAT2被展开后,这一步骤在此处不可见。

因此,在被替换之前,Y要完全展开,经历步骤1、2和3,我们在此省略,因为它显然展开为a。 因此,将a放入CAT2(x)中,得到:

CAT2(a)

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

步骤3:展开所有现有的宏。我们有宏CAT2(a),因此我们继续展开。

步骤3.1:在CAT2的参数x中出现了一个字符串化pref_ ## x。因此,将输入字符串a原样粘贴,得到:

pref_ ## a

步骤 3.2:字符串化:

pref_a

步骤3:展开任何其他宏。 pref_a 不是任何宏,所以我们完成了。

GCC参数预扫描文档

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

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

现在考虑:

#define f(x) (x + 1)

f(f(a))

它扩展为:

((a + 1) + 1)

不要无限制地进行,让我们来分解一下:

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

f的定义中,参数x不是f的定义(x + 1)中连接的一部分。因此,在替换之前,它首先被完全展开。

步骤1.1:根据步骤1、2和3,我们完全展开参数x = f(1),得到x = (a + 1)

现在回到步骤1,我们将完全展开的x参数等于(a + 1),并将其放入f的定义中,得到:

((a + 1) + 1)

第二步和第三步:没有字符串化和扩展的宏,因此不会发生太多事情。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接