说明 C 预处理器中 # 和 ## 的未指定相对计算顺序的示例

Lov*_*ure 6 c operator-precedence language-lawyer c-preprocessor token-pasting-operator

关于已接受答案的一些评论位于本问题帖子的底部。

\n
\n

问题陈述

\n

根据C标准(C17草案,6.10.3.2\xc2\xb62):

\n
\n

#[the]和运算符的求值顺序##未指定。

\n
\n

我正在寻找一个示例,其中此评估顺序很重要,并且没有其他未定义行为的实例并且没有错误。

\n

在花了一些时间处理这个问题之后,我怀疑以下方法可能有效:

\n
#define PRECEDENCETEST(a, b, c)  # a ## b\n\nPRECEDENCETEST(c, , d)\n
Run Code Online (Sandbox Code Playgroud)\n

(请注意,预处理器可以按如下方式运行:cppgcc -E(GCC),cl /E (MSVC);请参阅下面的可编译虚拟示例。另请注意,空宏参数仅自 C99 起才合法。)

\n

我的问题:根据 C 标准,这实际上可以作为一个相对评估顺序#和产生合法输出的示例吗?##正如我在本文底部所解释的那样,如果我理解正确的话,答案可能取决于标准是否允许在之后使用令牌#与最初指定的令牌不同。

\n

如果答案是“是(因为......)”,那么我们就找到了一个例子!如果答案是“不,你的例子不起作用(因为......)”,那么我稍后会想办法征求更好的例子。

\n

(请注意,该标准不要求编译器对#和具有绝对相对评估顺序##运算符具有绝对相对的求值顺序。顺序可以是:从左到右、从右到左、遵循某些其他逻辑或完全随机。)

\n

文档

\n

较旧的 GCC 文档(似乎最高版本为 6.5)指出

\n
\n

##该标准没有指定 \xe2\x80\x98 \xe2\x80\x99 运算符链的计算顺序,也没有#指定 \xe2\x80\x98 \xe2\x80\x99 是否在与 \xe2\x80\x98 相同的时间##。因此,您不应编写任何依赖于任何特定顺序的代码。如果需要的话,可以通过适当使用嵌套宏来保证顺序。

\n

这可能很重要的一个示例是粘贴参数 \xe2\x80\x98 1\xe2\x80\x99、\xe2\x80\x98 e\xe2\x80\x99 和 \xe2\x80\x98 -2\xe2\x80\x99。这对于从左到右粘贴来说没问题,但从右到左粘贴会产生无效标记 \xe2\x80\x98 e-2\xe2\x80\x99。

\n

GCC 3.0 同时评估 \xe2\x80\x98 #\xe2\x80\x99 和 \xe2\x80\x98 ##\xe2\x80\x99 并且严格从左到右。旧版本首先以不可靠的顺序评估所有 \xe2\x80\x98 #\xe2\x80\x99 运算符,然后评估所有 \xe2\x80\x98 ##\xe2\x80\x99 运算符。

\n
\n

(至于##中间段落中的唯一示例(即:)1##e##-21e不是有效的浮动常数(C17 草案,6.4.4.2),但它是有效的pp-number(“预处理编号”;C17 草案,6.4) .8) 因为solee是一个有效的非数字标识符。(预处理数字的存在是为了“将预处理器与数字常量的完全复杂性隔离开来”;请参阅其C预处理器的GNU文档。)也就是说,一个更好的例子是2##.##e3(对从左到右的标记串联有效,但不适用于从右到左的标记串联),改编自MISRA 讨论。)

\n

无论如何,维基百科在其有关C 预处理器的文章中声明了以下内容文章中声明了以下内容:

\n
\n

[F]类似功能宏展开发生在以下阶段:

\n
    \n
  1. 字符串化操作被其参数的替换列表的文本表示替换(不执行扩展)。
  2. \n
  3. 参数被替换为其替换列表(不执行扩展)。
  4. \n
  5. 串联操作被替换为两个操作数的串联结果(不扩展结果标记)。
  6. \n
  7. 源自参数的标记被扩展。
  8. \n
  9. 生成的令牌将正常扩展。
  10. \n
\n
\n

但是,我在 C 标准或 GNU CPP(C 预处理器,GCC 的一部分)文档中找不到对这种特定评估顺序的支持,截至提出这个问题时,其最新文档( GCC 13.2)在这里

\n

最重要的是,上述来源(包括 C17 标准)都没有提供类似函数的宏的示例,该宏将根据###替换列表和 的相对优先级评估不同的内容宏的

\n

我正在寻找不会导致未定义行为或错误的示例,因为看似有效的宏可能是难以发现的错误的潜在来源。在这方面重要的是以下两个限制:

\n
    \n
  • “如果[来自运算符]的替换结果#不是有效的字符串文字,则行为未定义。” (C17 草案,6.10.3.2 \xc2\xb62)
  • \n
  • “如果[标记与 ] 连接的结果##不是有效的预处理标记,则行为未定义。” (C17 草案,6.10.3.3 \xc2\xb63)
  • \n
\n

找个例子

\n

事实证明,寻找合适的例子是非常棘手的。

\n

一方面,我们正在考虑的字符串文字(C17 草案,6.4.5) \xe2\x80\x93 因为它们是应用#\xe2\x80\x93 的结果,几乎不能使用以下命令与其他任何内容连接##

\n
    \n
  • ##不能用于连接两个字符串文字,因为类似的东西"abc""def"不会是有效的预处理令牌(C17 草案,6.4 \xc2\xb61)。这里重要的是要注意,##基于 - 的标记串联与翻译阶段 6(C17 草案,5.1.1.2 \xc2\xb61)中的字符串文字的串联不同,后者会将"abc"和合并"def""abcdef".
  • \n
  • 字符串文字可以选择以编码前缀( u8, u, U, L) 开头,但是编写这样的替换列表[...] ## # b会导致有效的预处理标记需要#s 的微妙平衡(除了启动预处理指令或在字符串之外)或字符文字,只能作为预处理标记#及其##本身的一部分存在),这是我无法实现的。例如,\n
    #define TEST(a, b)  a ## # b\nTEST(, c)\n
    Run Code Online (Sandbox Code Playgroud)\n"c"在任一评估顺序下生成(假设#字符串化运算符可以合法地从应用中产生##),并且我不确定此示例是否可以变形为根据评估顺序生成两个不同的有效结果的示例。
  • \n
\n

另外,类似的东西a ## b # c不起作用,因为在这个表达式中,“ a ## b”和“# c ”部分是独立的。

\n

然而,似乎以下方法可能有效:

\n
#define PRECEDENCETEST(a, b, c)  # a ## b\n\nPRECEDENCETEST(c, , d)\n
Run Code Online (Sandbox Code Playgroud)\n

情况 A:使用 GCC 和 MSVC,我得到输出c,对应于#-before-##评估顺序

\n
\n

PRECEDENCETEST(c, , d)
\n # a ## b
\n "c" ## b
\n"c" ## <placemarker>
\n"c"

\n
\n

(地标预处理标记表示与 相邻的空宏参数##。(C17 草案,6.10.3.3 \xc2\xb62))

\n

情况 B:A ##-before-#评估顺序将为我们提供以下内容:

\n
\n

PRECEDENCETEST(c, , d)
\n # a ## b
\n # c ## <placemarker>
\n # c
\n"d"

\n
\n

也就是说,程序的输出必须是d. 或者会吗?这里的最后一步假设#不仅可以对原始替换列表中的参数进行操作,还可以对 的应用所产生的参数进行操作##。重要的是请注意以下约束(C17 草案,6.10.3.2 \xc2\xb61)

\n
\n

类函数宏的替换列表中的每个 # 预处理标记应后跟一个参数,作为替换列表中的下一个预处理标记。[这不适用于类似对象的宏。]

\n
\n

并没有违反\xe2\x80\x93,只是在这个例子中, 的实际参数#最终是c替换列表a)中指定的参数不同的参数( )。

\n
\n

关于已接受答案的评论:

\n
    \n
  • 我相信接受的答案代表了对该标准最合理的解释。事实上,该标准的编写方式应该迫使任何读者得出相同的结论。

    \n
  • \n
  • 然而,我确实相信该标准的作者没有经过深思熟虑。原因是这样的:结合

    \n
      \n
    1. 接受的答案
    2. \n
    3. 我的思考(在我的问题帖子“查找示例”部分开头的要点中)##两个字符串文字的串联
    4. \n
    \n

    比较接近证明证明

    \n
    \n

    #不存在相同输入以两种不同方式解析的情况,这两种方式仅在和 的顺序上有所不同##两种不同的输出可能性,而这两种不同的输出可能性既不会引发错误(例如违反预处理器约束)也不会引发未定义的行为。

    \n
    \n

    因为,如果确实不存在这样的情况,C 标准的编写者可以简单地规定“#之前##”,因为添加这样的规定不会影响现有的有效/非 UB 程序。(有关更多详细信息/要点,请参阅我与回答者的讨论。)

    \n

    同样,如果 C 标准像公认的答案所暗示的那样清晰,为什么 GCC 维护者和文档作者(显然对这个问题进行了一些思考)没有提供具有类似结论的相关评论(或其他对比示例)?

    \n
  • \n
\n

Joh*_*ger 6

#[the]和运算符的求值顺序##未指定。

我正在寻找一个示例,其中此评估顺序很重要,并且没有其他未定义行为的实例并且没有错误。

我认为您的意思是您正在寻找一种情况,其中两个不同的评估顺序产生有效(无错误)但语义不同(顺序很重要)的结果。

我怀疑以下方法可能有效:

#define PRECEDENCETEST(a, b, c)  # a ## b

PRECEDENCETEST(c, , d)
Run Code Online (Sandbox Code Playgroud)

[...]根据 C 标准,这实际上可以作为 # 和 ## 的相对求值顺序产生合法输出的示例吗?

不。

您需要特别注意宏参数处理的规范以及#and##运算符的行为。当扩展类似函数的宏时,宏的替换列表中参数名称的每次出现都存在三种可能的情况:

  1. 参数 [name]前面既不带###预处理标记,也不带##预处理标记(C17 6.10.3.1/1)。在这种情况下,相应参数的预处理标记序列被完全宏扩展,然后参数被结果替换。

  2. 参数 [name]紧接着是#预处理标记(C17 6.10.3.2/2)。在这种情况下,相应参数的预处理标记序列被字符串化,然后#参数被结果替换。

  3. 参数 [name] 的前面或后面紧跟着一个##预处理标记(C17 6.10.3.3/2)。在这种情况下,参数首先被替换为相应参数的预处理标记序列或地标记标记(视情况而定)。然后,在重新扫描之前但(隐式)在本段所需的参数替换之后,将适当的标记粘贴应用于预处理标记,将##替换列表中的每个标记括起来。

规范在这些子句中提到“参数”时,指的是替换列表中的单个预处理标记,其中包含参数名称,而不是相应参数的预处理标记。因此,只有其中一种情况可以应用于参数的任何给定外观。一旦根据这些规则之一将该外观替换为其他内容,则不再有需要根据其他规则之一替换的参数。

#您的示例涉及一个前面是 a 、后面是 a 的参数##,因此可以根据情况 (2) 或情况 (3) 执行其替换。我们可以争辩说,规范没有定义在这两种情况都适用的情况下会发生什么(因此是未定义的行为),但假设我们不去那里,而是查看评估顺序。然后,

  • 首先应用字符串化是有效的。和#a替换为字符串文字 token "c"。的操作数##不必是参数,因此没有参数保留为 的左侧操作数是可以的##。被b替换(然后或更早)为地标标记,并且在重新扫描之前的某个时间执行串联,产生"c"

  • ##首先应用操作员要求的令牌替换不起作用。执行该替换后,在操作评估期间不再有要替换的参数#。因此,选择这种评估顺序的结果是未定义的行为。

旁注:"d"即使我们在区分参数及其相应的参数序列方面更加宽松,您的示例也无法预期会产生结果。参数c不会出现在宏的替换列表中,因此其相应的参数对生成的扩展没有贡献。即使我们在c替换列表的末尾添加了 a,也没有理由认为字符串化运算符的范围会扩展到那么远,无论宏的第二个参数是什么。


#总的来说,如果和之间存在有意义的评估顺序选择##,则该选择是在首先字符串化的有条件未定义行为和首先粘贴的无条件未定义行为(或者至少首先执行该操作的参数替换部分)之间。由于即使是字符串化优先的情况也仅在运算符的另一个操作数##是一个参数且其对应的参数是空序列时才有效,因此形成这样的构造似乎没有什么意义。