我们应该如何解释带有嵌入式逗号的宏

How Should We Interpret a Macro with an Embedded Comma

本文关键字:嵌入式 解释 何解释 我们      更新时间:2023-10-16

我们应该如何使用C++标准来解释以下宏定义?请注意,主要问题是AA的替换列表包含嵌入的逗号 (for, S)

#define AA for, S    //<---note the embedded comma
#define VALUE_TO_STRING(x) ^x!
#define VALUE(x) VALUE_TO_STRING(x)
int _tmain(int argc, _TCHAR* argv[])
{
VALUE(AA)
return 0;
}

我已经用VC++2010做了一个测试,上面的最终结果看起来像下面,没有任何错误,但我在解释使用C++03(或C++11)标准得出结果的步骤时遇到了问题:

int wmain(int argc, _TCHAR* argv[])
{
^for, S!
return 0;
}

我已经用VC++2010做了一些分步测试。首先,我注释掉了第二个宏,看看第一步发生了什么:

#define AA for, S
//#define VALUE_TO_STRING(x) ^x!
#define VALUE(x) VALUE_TO_STRING(x)

宏替换是直接的,并生成了一个序列,看起来像另一个具有两个参数的类似函数的宏:

int wmain(int argc, _TCHAR* argv[])
{
VALUE_TO_STRING(for, S)
return 0;
}

根据[cpp.rescan]的说法,下一步是重新扫描它以获取更多的宏名称。这里的问题是这个新的宏应该被解释为一个具有 2 个参数或 1 个参数"for, S"的类似函数的宏。

正常的解释是考虑VALUE_TO_STRING()给定2个无效的参数,因此导致预处理器错误。但是为什么VC++得出的结果没有任何错误呢?显然,VC++ 采取的第二步是将for, S视为 1 个没有意义的单个参数,并且没有由C++标准定义。

我已经用VC++2010做了一个测试...

MS的预处理器从未成为标准处理器。 他们这样奇怪地表达它:

C99 __func__和预处理器规则... 对于 C99 预处理器规则,列出了"部分",因为支持可变参数宏。

换句话说,"我们支持可变参数宏;因此,我们有资格成为部分合规"。 预处理器的 AFAIK 标准合规性被 MS 团队视为优先级非常低。 所以我不倾向于使用 VC 或 VC++ 作为标准预处理器的模型。 GCC 是标准预处理器的更好模型。

由于这是关于预处理器的,我将把故事集中在这个片段上:

#define AA for, S
#define VALUE_TO_STRING(x) ^x!
#define VALUE(x) VALUE_TO_STRING(x)
VALUE(AA)

我将在这里引用ISO-14882 2011,它使用与1998/2003不同的数字。 使用这些数字,以下是从扩展步骤开始,逐步发生的事情......除了我将跳过的此处不相关的步骤。

预处理器看到VALUE(AA),这是对先前定义的类似函数的宏的类似函数的调用。 所以它做的第一件事是参数识别,参考 16.3 第 4 段:

[如果不是可变的]调用类似函数的宏中的参数数(包括那些不包含预处理标记的参数)应等于宏定义中的参数数

......以及16.3.1第1款的一部分:

确定了调用类似函数

的宏的参数后,在此步骤中,预处理器识别确实存在一个参数,该宏是用一个参数定义的,并且参数x与调用参数AA匹配。 到目前为止,参数匹配和x is AA就是发生的一切。

然后我们进入下一步,即参数扩展。 关于这一步,关于替换列表唯一真正重要的是参数在其中的位置,以及参数是字符串化(# x)还是粘贴(x ## ...... ## x)的一部分。 如果替换列表中的参数两者都不是,则会展开这些参数(在此步骤中不计算参数的字符串化或粘贴版本)。 此扩展首先发生,在调用中发生任何其他有趣的事情之前,并且它就像预处理器仅扩展调用参数一样。

在这种情况下,替换列表为VALUE_TO_STRING(x)。 同样,VALUE_TO_STRING可能是一个类似函数的宏,但由于我们现在正在进行参数扩展,我们真的不在乎。 我们唯一关心的是x在那里,它没有被字符串化或粘贴。x是用AA调用的,所以预处理器计算AA就好像AA在一行而不是VALUE(AA)上一样。AA是一个类似对象的宏,可扩展到for, S. 所以替换列表变成了VALUE_TO_STRING(for, S).

这是16.3.1第1段的其余部分:

替换列表中的参数,除非 [字符串化或粘贴] 在其中包含的所有宏都已展开后被相应的参数替换 [...],就好像它们构成了预处理文件的其余部分

一样 到目前为止一切顺利。 但是现在我们进入下一部分,在 16.3.4 中:

替换
列表中的所有参数都被替换并且[此处未发生]生成的预处理令牌序列之后 重新扫描源文件的所有后续预处理令牌,以替换更多宏名称。

这部分评估VALUE_TO_STRING(for, S),就好像那是预处理令牌集一样(除了它也暂时忘记了VALUE是每 16.3.4p2 的宏,但这在这里没有发挥作用)。 该计算将VALUE_TO_STRING识别为类似函数的宏,并像宏一样被调用,因此参数标识再次开始。 只有在这里,VALUE_TO_STRING被定义为接受一个参数,但被调用两个参数。 这失败了 16.3 p 4。

我认为答案是按扩展顺序排列的。

在我看来,您对预处理器扩展的模拟,即您选择首先扩展哪个宏,与预处理器的功能不匹配。

我作为预处理器(根据我最初相信的标准;但注释相互矛盾),将按以下顺序扩展您的代码:

VALUE(AA)
VALUE_TO_STRING(AA)
^AA!
^for, S!

这与原始代码的预处理器的结果匹配。 注意按照这个顺序它永远不会看到代码VALUE_TO_STRING(for, S),它得到的最接近的是VALUE_TO_STRING(AA)。该代码不会导致有关参数数量的问题。

我没有引用标准中的任何内容,我认为您的报价就足够了。

正如下面的评论中提到的,我的答案现在是尝试如何解释结果,而不假设符合预处理器。任何用顺从行为解释的答案肯定更好。

顺便说一句,作为编译器,我可能不会将^anything!理解为
从值生成字符串的一种方式。但这不是问题,我认为当你准备最小的例子时,意义已经丢失了。这当然是完全可以的。但是,如果它扩展到引用的宏名称,则可能会影响扩展,例如"AA".这将停止扩大,结果可能会揭示发生的事情。