1. 剖析错误
让我们从这里开始:
#define BAR(x) FOO(x, BAR_
#define BAR_(y) y)
#define FOO(x,y) foo{x|y}
BAR(1)(2)
请注意,我只会使用预处理器来调试预处理器(当我可以简单地调用预处理器时,为什么我需要构建调用预处理器的 C++ 程序?(这是修辞性的 o/c;我是只是在这里讲一个故事))。
CPP 是这样看待这个问题的。我们看BAR
;这是一个类似函数的宏。有趣,但除非我们看到左括号,否则不可操作。接下来,我们看到...一个左括号。现在我们已经取得进展了……我们需要确定它的论点。所以我们继续扫描:BAR(1)
...那里...那是匹配的左括号...看起来我们正在用一个参数调用。BAR
事实证明它是用一个参数定义的,所以这很有效。
现在我们表演参数替换...所以我们注意到BAR
的替换列表提到了它的参数(x
)以非字符串化、非粘贴的方式。这意味着我们应该评估相应的参数(1
),这很容易......这就是它本身。然后,该评估结果将替换替换列表中的参数,因此我们有FOO(1, BAR_
。现在我们已经完成了参数替换。
接下来我们需要做的是重新扫描并进一步更换。所以我们重新扫描...FOO
, 是啊。这是一个类似函数的宏...FOO(
...并且它正在被调用。现在我们已经取得进展了……我们需要确定它的论点。所以,我们继续扫描:FOO(1, BAR_(2)
...然后,突然我们到达了文件的末尾。啊?那是不对的。FOO
正在被调用;它应该有一个匹配的括号。
你可能天真地认为BAR_(2)
应该被调用,但这不是宏的工作方式。他们评估外在和“内”(又名,参数标记)only评估替换列表中是否提及该参数,其中所述提及不是字符串化或粘贴。
请注意,如果FOO
如果不是一个类似函数的宏,这个故事将会走向完全不同的方向。在这种情况下,FOO(
只是预处理器不关心的标记......所以当它看到时BAR_(2)
, it will调用宏。但还有另一个“骗局”:如果FOO
被忽略而没有实际调用宏FOO
,代币将also被跳过。在这两种情况下,FOO(1, 2)
最终会被生产出来,这就是你想要的。但如果你随后想要evaluate FOO
作为一个类似函数的宏,你只有一个选择。你需要第二次通过;第一遍实际上允许调用序列中的第二个参数来构建宏,并且此遍must not allow FOO
被调用。这second需要 pass 才能调用它。
2. 如何做到这一点
嗯,这很简单:
#define DELAY()
#define BAR(x) FOO DELAY() (x, BAR_
#define BAR_(y) y)
#define FOO(x,y) foo{x|y}
#define EVAL(...) __VA_ARGS__
BAR(1)(2)
EVAL(BAR(1)(2))
这是不同之处(第一行)。后BAR
的参数替换,它的替换列表现在是FOO DELAY() (1, BAR_
而不仅仅是FOO(1, BAR_
。现在,在重新扫描期间,它仍然看到FOO
,这仍然很有趣......但它看到的下一个标记是DELAY
,不是左括号。所以 CPP 在这一次的决定中not调用FOO
并传承下去。满后DELAY()
膨胀,不会产生任何东西,然后它只会看到(1, BAR_
;前三个只是令牌。BAR_
然而,是一个宏调用,故事也是如此......所以扩展的结果BAR(1)(2)
是生产代币FOO(1, 2)
,无错误。但这并不能评价FOO
.
The EVAL
,但是,接受BAR(1)(2)
作为一个论点。它的替换列表提到了它的“参数”(不同的 arg 变体),所以BAR(1)(2)
被充分评估,产生FOO(1, 2)
. Then FOO(1, 2)
被重新扫描,此时FOO
实际上被调用。