【问题标题】:How Should We Interpret a Macro with an Embedded Comma我们应该如何解释带有嵌入式逗号的宏
【发布时间】:2017-03-26 12:12:42
【问题描述】:

我们应该如何使用 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 视为一个没有意义且未由 C++ 标准定义的单个参数。

【问题讨论】:

  • 为什么要投反对票???
  • 我没有投反对票。我刚刚对这个问题投了赞成票,在我看来这个问题很难,但很清楚也很有用。
  • VC++ 没有符合标准的预处理器。

标签: c++ c-preprocessor language-lawyer


【解决方案1】:

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

MS 的预处理器从未成为标准。 They phrase it this odd way:

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

换句话说,“我们支持可变参数宏;因此我们符合部分合规的条件”。预处理器的 AFAIK 标准合规性被 MS 团队认为是非常低的优先级。所以我不会倾向于使用 VC 或 VC++ 作为标准预处理器的模型。 gcc 是这里标准预处理器的更好模型。

因为这是关于预处理器的,所以我将把故事重点放在这个 sn-p 上:

#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 就是所有发生的事情。

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

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

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

替换列表中的一个参数,除非 [stringified or paste] 在其中包含的所有宏都已展开 [...] 后被相应的参数替换,就好像它们构成了预处理文件的其余部分一样

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

在替换列表中的所有参数都被替换并且 [这里没有发生的事情] 生成的预处理标记序列之后 与源文件的所有后续预处理标记一起重新扫描,以替换更多宏名称。

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

【讨论】:

    【解决方案2】:

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

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

    我,作为一个预处理器(根据我最初相信的标准;但有评论相矛盾),将按此顺序扩展您的代码:

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

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

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

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

    顺便说一句,作为编译器,我可能不会将
    ^anything! 理解为一种从值生成字符串的方法。但这不是问题所在,当您准备最小示例时,我认为含义已经丢失。这当然完全没问题。但是,它可能会影响扩展,如果它曾经扩展为引用的宏名称,例如"AA"。这将停止扩展,结果可能会揭示发生了什么。

    【讨论】:

    • 您描述的步骤不太可能由标准一致性编译器执行。由于段落 [cpp.subst] 规定:“在被替换之前,每个参数的预处理标记都被完全宏替换......”。所以“AA”应该在VALUE之前先被宏替换
    • 其实我把我的代码改成了^!以避免 # 运算符出现问题。我第一次使用的代码是这样的 #define VALUE_TO_STRING(x) #x
    • @JavaMan 我接受你的论点;学到东西了,谢谢因此,如果预处理器像我一样工作,那么它不符合要求。我将留下答案作为对意外结果的可能解释。如果任何答案都有基于标准的解释,那么它绝对值得它收到的所有赞成票。
    • 考虑到@JavaMan 的注释,宏VALUE(AA) 是否有可能被识别为已定义,包括它提供单个参数的事实?然后首先(递归)扩展参数,然后扩展为VALUE(singleparameter&lt;for, S&gt;)。其中 singleparameter 不是宏,而是“已经扩展的参数”。这是否与标准相矛盾?我对拥有知识和玩 advocatus diaboli 表示敬意。
    • @JavaMan #... 替换为 ^...! 是我的想象。我故意回答,尽量不做出这样的假设。正如我所说,这似乎是合适的,不会影响预处理。 IE。一个很好的小例子。
    猜你喜欢
    • 1970-01-01
    • 2020-02-02
    • 1970-01-01
    • 2019-03-16
    • 2018-01-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-03-28
    相关资源
    最近更新 更多