提问人:Lover of Structure 提问时间:10/8/2023 最后编辑:Lover of Structure 更新时间:10/21/2023 访问量:207
举例说明 C 预处理器中 # 和 ## 的未指定相对计算顺序
Example illustrating the unspecified relative evaluation order of # and ## in the C preprocessor
问:
关于已接受答案的一些评论在本问题帖子的底部。
问题陈述
根据 C 标准(C17 草案,6.10.3.2 ¶2):
[the] 和运算符的计算顺序未指定。
#
##
我正在寻找一个示例,其中此评估顺序很重要,并且没有其他未定义行为的实例,也没有错误。
在花了一些时间讨论这个问题之后,我怀疑以下方法可能会起作用:
#define PRECEDENCETEST(a, b, c) # a ## b
PRECEDENCETEST(c, , d)
(请注意,预处理器可以按如下方式运行:或 (GCC)、(MSVC);有关可编译的虚拟示例,请参阅下文。另请注意,空宏参数仅在 C99 之后才合法。cpp
gcc -E
cl /E
我的问题是:根据 C 标准,这是否真的可以作为 # 和 ##
的相对评估顺序产生合法输出的示例?正如我在这篇文章底部所解释的那样,如果我理解正确的话,答案可能取决于标准是否允许之后的令牌最终与最初指定的令牌不同。
#
如果答案是“是(因为...)”,那么我们找到了一个例子!如果答案是“不,你的例子不起作用(因为......)”,那么我稍后会想出一种方法来征求更好的例子。
(请注意,该标准不要求编译器对 and 运算符具有绝对相对计算顺序。顺序可以是:从左到右,从右到左,遵循其他逻辑,或者完全随机。#
##
文档
较旧的 GCC 文档(似乎最高为 6.5 版)指出:
该标准没有规定“”运算符链的计算顺序,也没有规定“”是在“”之前、之后还是与“”同时计算。因此,您不应编写任何依赖于任何特定顺序的代码。如果需要,可以通过适当使用嵌套宏来保证排序。
##
#
##
例如,粘贴参数 ''、'' 和 ''。这对于从左到右的粘贴来说很好,但从右到左的粘贴会产生无效的标记 ''。
1
e
-2
e-2
GCC 3.0 同时计算 '' 和 '',严格从左到右。旧版本首先计算所有 '' 运算符,然后以不可靠的顺序计算所有 '' 运算符。
#
##
#
##
(对于中间段落中的 -only 示例(即:):不是有效的浮点常数(C17 draft,6.4.4.2),但它是有效的 pp 数(“预处理数”;C17 草案,6.4.8),因为鞋底是有效的标识符非数字。(预处理数字的存在是为了“将预处理器与数字常量的全部复杂性隔离开来”;参见 GNU 文档了解其 C 预处理器。也就是说,一个更好的例子是(对从左到右有效,但不适用于从右到左的令牌连接),改编自这个 MISRA 讨论。##
1##e##-2
1e
e
2##.##e3
就其价值而言,维基百科在其关于C预处理器的文章中声称以下内容:
类似宏扩展的扩展发生在以下阶段:
- 字符串化操作将替换为其参数替换列表的文本表示形式(不执行扩展)。
- 参数将替换为其替换列表(不执行扩展)。
- 串联运算将替换为两个操作数的串联结果(不扩展生成的标记)。
- 源自参数的令牌将展开。
- 生成的令牌将照常展开。
但是,我在 C 标准或 GNU 的 CPP(C 预处理器,GCC 的一部分)文档中找不到对这种特定评估顺序的支持,截至提出这个问题时,其最新文档(GCC 13.2)在这里。
最重要的是,上述来源(包括 C17 标准)都没有提供类似函数的宏的示例,该宏的计算结果会根据宏替换列表中 # 和 ##
的相对优先级而有所不同。
我正在寻找不会导致其他未定义行为或错误的示例,因为看似有效的宏是难以发现的错误的潜在来源。在这方面,重要的是以下两个约束:
- “如果 [来自运算符] 的替换不是有效的字符串文字,则行为未定义。”(C17 草案,6.10.3.2 ¶2)
#
- “如果 [令牌连接] 的结果不是有效的预处理令牌,则行为未定义。”(C17 草案,6.10.3.3 ¶3)
##
查找示例
寻找合适的例子结果出乎意料地棘手。
首先,字符串文字(C17 draft,6.4.5)——我们正在考虑,因为它们是应用的结果——几乎不能与其他任何内容连接起来:#
##
##
不能用于连接两个字符串文字,因为类似的东西不是有效的预处理标记(C17 草案,6.4 ¶1)。这里需要注意的是,基于标记的串联与翻译阶段 6(C17 草案,5.1.1.2 ¶1)中的字符串文字串联不同,后者将合并并合并为 ."abc""def"
##
"abc"
"def"
"abcdef"
- 字符串文字可以选择以编码前缀 (, , , ) 开头,但编写这样的替换列表会导致有效的预处理标记需要 s 的微妙平衡(除了启动预处理指令或在字符串或字符文字中之外,它只能作为预处理标记和它们本身的一部分存在),我无法实现。例如,在任一计算顺序下生成(假设字符串化运算符可以合法地从应用 中得到),我不确定这个示例是否可以根据计算顺序演变成一个产生两个不同的有效结果。
u8
u
U
L
[...] ## # b
#
#
##
#define TEST(a, b) a ## # b TEST(, c)
"c"
#
##
此外,类似的东西不起作用,因为在这个表达式中,“”和“”部分是独立的。a ## b # c
a ## b
# c
但是,似乎以下方法可能有效:
#include <stdio.h>
#define PRECEDENCETEST(a, b, c) # a ## b
int main(void) {
printf("%s\n", PRECEDENCETEST(c, , d));
return 0;
}
案例 A:对于 GCC 和 MSVC,我得到的输出对应于 #-before-#
#
的评估顺序:c
PRECEDENCETEST(c, , d)
# a ## b
"c" ## b
"c" ## <placemarker>
"c"
(地标预处理标记表示与 相邻的空宏参数。(C17 草案,6.10.3.3 ¶2))##
案例 B:##
-before-#
评估顺序将给出以下内容:
PRECEDENCETEST(c, , d)
# a ## b
# c ## <placemarker>
# c
"d"
也就是说,程序的输出必须是 。或者会吗?最后一步假定它不仅可以对原始替换列表中的参数进行操作,还可以对应用 .需要注意的是,以下约束(C17 草案,6.10.3.2 ¶1)d
#
##
类似函数的宏的替换列表中的每个 # 预处理标记后应跟一个参数,作为替换列表中的下一个预处理标记。[这不适用于类似对象的宏。
没有被违反 - 只是在这个例子中,实际参数最终是与替换列表()中指定的参数不同的参数()。#
c
a
关于接受答案的评论:
我相信公认的答案代表了对标准最明智的解释。事实上,该标准的编写方式应该迫使任何读者得出相同的结论。
但是,我确实相信该标准的作者没有考虑清楚。究其原因,是这样的:结合
- 接受的答案和
- 我的思考(在我的问题帖子的“寻找示例”部分开头的项目符号中)关于两个字符串文字的串联
##
相对接近于证明
不存在以下情况:以两种不同的方式解析相同的输入,仅在应用顺序上有所不同,从而导致两种不同的输出可能性,这些输出既不调用错误(例如违反预处理器约束),也不调用未定义的行为。
#
##
因为,如果确实没有这样的情况,C标准的作者可以简单地规定“之前”,因为添加这样的规定将无法影响现有的有效/非UB程序。(有关更多详细信息/要点,请参阅我与回答者的讨论。
#
##
同样,如果 C 标准像公认的答案所暗示的那样清晰,为什么 GCC 维护者和文档作者(他们显然对这个问题进行了一些思考)没有提供具有类似结论(或其他对比示例)的相关评论?
答:
[the] 和运算符的计算顺序未指定。
#
##
我正在寻找一个示例,其中此评估顺序很重要并且 其中没有其他未定义行为的实例,并且没有 错误。
我认为你的意思是,你正在寻找一个案例,其中两个不同的评估顺序产生有效(没有错误)但在语义上不同(顺序很重要)的结果。
我怀疑以下方法可能有效:
#define PRECEDENCETEST(a, b, c) # a ## b PRECEDENCETEST(c, , d)
[...]这是否实际上可以作为示例,其中任何一个相对 # 和 ## 的计算顺序产生合法输出,根据 C 标准?
不。
您需要特别注意宏参数处理的规范和 and 运算符的行为。展开类似函数的宏时,参数名称在宏的替换列表中的每次出现都有三种可能的情况:#
##
参数 [name] 前面既不是 # 或 ## 预
处理令牌,也不是 #
#
预处理令牌(C17 6.10.3.1/1)。在这种情况下,对应参数的预处理标记序列被完全宏展开,然后参数被结果替换。
参数 [name] 前面紧跟一个
#
预处理标记 (C17 6.10.3.2/2)。在这种情况下,对应参数的预处理标记序列进行字符串化,然后将 和 参数替换为结果。#
参数 [name] 紧跟在 #
#
预处理标记 (C17 6.10.3.3/2) 的前面或后面。在这种情况下,参数首先替换为相应参数的预处理标记序列或占地标记标记(视情况而定)。然后,在重新扫描之前,但(隐式)在本段要求的参数替换之后,将适当的令牌粘贴应用于替换列表中每个标记的预处理标记。##
如果规范在这些子句中说“参数”,则它谈论的是替换列表中包含参数名称的单个预处理令牌,而不是相应参数的预处理标记。因此,这些情况中只有一种可以应用于参数的任何给定外观。一旦根据这些规则之一将该外观替换为其他内容,则不再有要根据其他规则之一替换的参数。
您的示例涉及一个参数,该参数前面是 a,后跟 ,因此可以根据情况 (2) 或情况 (3) 执行其替换。我们可以争辩说,规范没有定义在这两种情况都适用的情况下会发生什么(因此行为未定义),但假设我们不去那里,而是查看评估顺序。然后#
##
首先应用字符串化。和替换为字符串文字标记。的操作数不必是参数,因此没有参数作为 的左操作数是可以的。替换为(当时或更早)的地标标记,并在重新扫描之前的某个时间执行串联,从而生成 .
#
a
"c"
##
##
b
"c"
首先应用操作员要求的令牌替换不起作用。执行该替换后,在操作评估期间不再需要替换参数。因此,选择此评估顺序的结果是未定义的行为。
##
#
旁注:即使我们在区分参数及其相应的参数序列方面要宽松得多,也不可能期望您的示例产生。参数不会出现在宏的替换列表中,因此其对应的参数对生成的扩展没有贡献。即使我们在替换列表的末尾加上了 a,也没有理由认为字符串化运算符的范围会扩展到那么远,无论宏的第二个参数是什么。"d"
c
c
总的来说,如果在 和 之间有一个有意义的评估顺序选择,那么这个选择就是在有条件的未定义行为(从第一个字符串化)到无条件的未定义行为(或至少首先执行该行为的参数替换部分)之间。由于即使是字符串化优先的情况也只有在运算符的另一个操作数是其对应参数为空序列的参数时才有效,因此形成此类结构似乎意义不大。#
##
##
评论
##
#
# 的运算符”(¶4),但另一方面在 6.10.3.2 中没有对 #
进行类似的澄清。类似地指出,运算符及其参数参数都不能源自参数或基于串联的结果,这很容易,即使它的一部分来自标准,也建议说明这一点。...hash_hash
##
#
##
##
##
#
#
评论
#
##
##
#
"d"