C预处理器,递归宏

19
为什么M(0)和N(0)的结果不同?
#define CAT_I(a, b) a ## b
#define CAT(a, b) CAT_I(a, b)

#define M_0 CAT(x, y)
#define M_1 whatever_else
#define M(a) CAT(M_, a)
M(0);       //  expands to CAT(x, y)

#define N_0() CAT(x, y)
#define N_1() whatever_else
#define N(a) CAT(N_, a)()
N(0);       //  expands to xy

你到底想要实现什么,目的是什么? - t0mm13b
20
我并不真的想取得什么成就,只是在处理某件事时注意到了这一点,我对其中的原因很好奇。当我不理解某件事情时,这会让我感到烦恼 :)。 - imre
3个回答

20
事实上,这取决于您对语言标准的解释。例如,在严格符合语言标准文本的预处理器实现mcpp下,第二个也会产生CAT(x, y);的结果[已从结果中删除额外换行符]:
C:\dev>mcpp -W0 stubby.cpp
#line 1 "C:/dev/stubby.cpp"
        CAT(x, y) ;
        CAT(x, y) ;
C:\dev>

C++语言规范存在已知的不一致性(尽管我不知道C规范的缺陷列表在哪里,但同样存在这种不一致性)。规范说明最终的CAT(x, y)不应被宏替换。其目的可能是应该被宏替换。

引用相关缺陷报告:

回到20世纪80年代,WG14的几个人理解到,“非替换”的措辞与尝试生成伪代码之间存在微小差异。

委员会的决定是,在实际程序中没有进入这个领域的意义,试图减少不确定性并不值得改变实现或程序的符合性状态的风险。


因此,为什么在大多数常见的预处理器实现中,我们针对M(0)N(0)获得不同的行为?在替换M时,第二次调用CAT完全由第一次调用CAT产生的标记组成:
M(0) 
CAT(M_, 0)
CAT_I(M_, 0)
M_0
CAT(x, y)

如果将M_0定义为替换为CAT(M,0),则替换会无限递归。预处理器规范通过停止宏替换来禁止这种“严格递归”替换,因此CAT(x,y)不会被宏替换。然而,在替换N时,第二次调用CAT仅部分包含第一次调用CAT的结果产生的标记。
N(0)
CAT(N_, 0)       ()
CAT_I(N_, 0)     ()
N_0              ()
CAT(x, y)
CAT_I(x, y)
xy

这里第二次调用CAT部分由第一次调用CAT的标记组成,部分由其他标记组成,即来自N替换列表的()。替换不是严格递归的,因此当第二次调用CAT被替换时,它不能产生无限递归。


有趣的是,VC++中的预处理器和在线Comeau编译器都将N(0)扩展为“xy”。 - imre
1
我有一个模糊的记忆,因为提供给N_0()来自于任何宏展开之外,所以这算作是一个新的宏展开,因此“蓝色涂料”会从CAT()中移除,它可以再次被展开。所以这可能是mcpp中的一个错误。值得一提的是,gcc与Comeau和VC++持相同观点。 - zwol
@imre:我很有兴趣知道为什么在dllimport / dllexport declspec中需要如此复杂的东西。惯用法是使用单个宏(例如MYPROJECT_EXPORT),根据正在构建的是“My Project”来有条件地设置其中之一。 - James McNellis
1
顺便提一下,重要的是要注意mcpp在CPP验证套件上得分完美...由mcpp的作者编写。因此,所有这些分数显示的只是mcpp执行其作者认为应该执行的操作;它并没有表明它实际上忠实于C标准。 - Jim Balter
1
@Jim:我建议阅读mcpp测试套件文档,其中包含了一个八页的讨论,解释了规范中的矛盾以及规范变化的方式。在C99中,行为明确未指定。符合规范的实现可以替换CAT的第二次调用,也可以不替换。 - James McNellis
显示剩余6条评论

3

只需按照顺序进行:

1.)

M(0); //  expands to CAT(x, y) TRUE 
CAT(M_, 0)
CAT_I(M_, 0)
M_0
CAT(x, y)

2.)

N(0); //  expands to xy TRUE
CAT(N_, 0)()
CAT_I(N_, 0)()
N_0()
CAT(x, y)
CAT_I(x, y)
xy

你只需要递归地替换宏。
##预处理器运算符的注意事项: 使用##预处理器运算符可以将两个参数“粘合”在一起;这允许在预处理代码中连接两个标记。
与标准宏展开不同,传统的宏展开没有防止递归的规定。如果一个对象宏在其替换文本中未加引号,则在重新扫描过程中它将再次被替换,依此类推。GCC检测到正在扩展递归宏时,会发出错误消息,并在有问题的宏调用后继续执行。(gcc online doc

1
嗯...我还是不明白。这两个序列都到达了相同的CAT(x, y) -- 那么为什么在一个情况下停止而在另一个情况下不停止呢? - imre
我认为这里的递归取决于标准的解释,就像James McNellis所说的那样。imre,好问题。 - Cacho Santa
1
M(0)的情况下,第二个CAT(...)调用完全是由第一个CAT(...)调用引起的,因此它是一个严格的递归调用。在N(0)的情况下,第二个CAT(...)调用仅部分地由第一个CAT(...)调用引起,另一部分来自于该调用之后出现的其他标记(即N的替换列表中的())。因此,它不是完全递归的。 - James McNellis

-1

看起来你可能没有注意到,但是你的宏有 N(a) CAT(N_,a)(),而 M(a) 被定义为 CAT(M_, a)。请注意使用了额外的参数括号....


1
我知道。相应地,N_0 定义为一个函数式(零参数)宏。由于某些原因,在递归评估中似乎会产生影响,但我不确定原因;这是我的问题。 - imre

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