【问题标题】:C preprocessor, recursive macrosC 预处理器,递归宏
【发布时间】:2011-04-12 21:27:14
【问题描述】:

为什么 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

【问题讨论】:

  • Uhhh.... 你到底想在这里实现什么... 目的是什么?
  • 我并不想实现任何目标,只是在做某事时注意到了这一点,我很好奇其中的原因。当我不明白某些事情时,我会很烦:)。

标签: macros c-preprocessor


【解决方案1】:

实际上,这取决于您对语言标准的解释。例如,在 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++ 语言规范中有a known inconsistency(C 规范中存在相同的不一致,尽管我不知道 C 的缺陷列表在哪里)。规范指出最终的CAT(x, y) 不应被宏替换。其意图可能是应该对其进行宏替换。

引用链接的缺陷报告:

早在 1980 年代,几位 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”。
  • 另外,是否有可能解决这个递归限制并让最后一个 CAT 进行评估? (除了定义另一种替代 CAT 之外?)
  • 我有一个模糊的记忆,因为提供给 N_0() 来自任何宏扩展之外,这算作 new 宏扩展,所以“蓝色油漆”脱落CAT(),它可以再次扩展。所以这可能是 mcpp 中的一个错误。 FWIW gcc 同意 Comeau 和 VC++。
  • 顺便说一句,重要的是要注意 mcpp 在 CPP 验证套件上的得分非常好......由 mcpp 的作者编写。因此,所有分数都表明 mcpp 做了作者认为它应该做的事情。它并不表明它实际上忠实于 C 标准。
  • @Jim:我建议阅读 mcpp 测试套件文档,其中包含关于该主题的八页讨论,并解释了规范中的矛盾以及规范更改的方式。在 C99 中,行为是明确未指定的。符合要求的实现可能会替换 CAT 的第二次调用,也可能不会。
【解决方案2】:

按照顺序:

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)

【讨论】:

  • 嗯...我还是不明白。这两个序列都到达相同的 CAT(x, y) —— 那么为什么在一种情况下停在那里而不在另一种情况下停在那里呢?
  • 我认为这里的递归取决于 James McNellis 所说的标准的解释。很好的问题。
  • @imre:在M(0) 的情况下,第二次CAT(...) 调用完全来自第一次CAT(...) 调用,因此它是一个严格的递归调用。对于N(0),第二个CAT(...) 调用仅部分来自第一个CAT(...) 调用,部分来自之后出现的其他标记(N 的替换列表中的())。因此,它不是完全递归的。
【解决方案3】:

您似乎没有发现某些东西,但您的宏有 N(a) CAT(N_,a)(),而 M(a) 被定义为 CAT(M_, a) 请注意使用的额外参数括号....

【讨论】:

  • 我知道。相应地,N_0 被定义为函数式(0 参数)宏。出于某种原因,这似乎对递归评估产生了影响,但我不知道究竟是为什么;这是我的问题。
猜你喜欢
  • 2012-11-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-02-09
  • 1970-01-01
  • 2010-10-23
  • 2011-01-26
  • 1970-01-01
相关资源
最近更新 更多