有效和无效 pp-token 的定义是什么?

What are the definitions for valid and invalid pp-tokens?

我想广泛使用 ## 运算符和枚举魔法来处理大量类似的访问操作、错误处理和数据流。

如果应用 ### 预处理器运算符导致无效的 pp-token,则该行为在 C 中未定义。

C90 中未定义 (*) 预处理器操作的一般顺序(参见 The token pasting operator)。现在在某些情况下,多个 ##/#-Operators 的顺序会影响未定义行为的发生(在不同的来源中这样说,包括 MISRA 委员会和参考页面)。但是我真的很难理解这些来源的例子并确定通用规则。

所以我的问题是:

  1. 有效 pp-token 的规则是什么?

  2. 不同的 C 和 C++ 标准之间有区别吗?

  3. 我当前的问题:以下所有 2 个操作员指令是否合法?(**)

    #define test(A) test_## A ## _THING
    int test(0001) = 2;
    

评论:

(*) 我不使用 "is undefined" 因为这与尚未定义的行为无关,恕我直言,而是未指定的行为。应用多个## 或# 运算符通常不会使程序出错。显然有一个顺序——我们只是无法预测是哪个——所以这个顺序是未指定的。

(**) 这不是编号的实际应用。但是模式是等价的。

有效 pp-tokens 的规则是什么?

这些在各自的标准中都有规定; C11 §6.4 和 C++11 §2.4。在这两种情况下,它们都对应于生产 preprocessing-token。除了 pp-number,它们应该不会太令人惊讶。剩下的可能性是标识符(包括关键字)、“标点符号”(在 C++ 中,preprocessing-op-or-punc)、字符串和字符文字,以及任何单个非空白字符'匹配任何其他产品。

除了少数例外,任何字符序列都可以分解为一系列有效的预处理标记。 (一个例外是不匹配的引号和撇号:单引号或撇号不是有效的 预处理标记 ,因此无法标记包含未终止字符串或字符文字的文本。)

但是,在 ## 运算符的上下文中,串联的结果必须是单个 preprocessing-token。因此,无效串联是一种串联,其结果是包含多个 preprocessing-tokens.

的字符序列

C 和 C++ 之间有区别吗?

是的,略有不同:

  • C++ 具有用户定义的字符串和字符文字,并允许“原始”字符串文字。这些文字将在 C 中以不同方式标记化,因此它们可能是多个 preprocessing-tokens 或者(在原始字符串文字的情况下)甚至无效 preprocessing-tokens.

  • C++ 包括符号 ::.*->*,所有这些都将标记为两个 标点符号 标记在 C 中。此外,在 C++ 中,一些看起来像关键字的东西(例如 newdelete)是 preprocessing-op-or-punc[=131= 的一部分](尽管这些符号在两种语言中都是有效的 preprocessing-tokens。)

  • C 允许十六进制浮点文字(例如 1.1p-3),这在 C++ 中是无效的 preprocessing-tokens

  • C++ 允许在整数文字中使用撇号作为分隔符 (1'000'000'000)。在 C 中,这可能会导致不匹配的撇号。

  • 在处理通用字符名称(例如 \u0234)方面存在细微差别。

  • 在 C++ 中,<:: 将被标记为 <::,除非其后跟 :>。 (<:::<::> 被正常标记化,使用最长匹配规则。)在 C 中,最长匹配规则没有例外; <:: 始终使用最长匹配规则进行标记化,因此第一个标记将始终为 <:.

即使未指定串联顺序,串联 test_0001_THING 是否合法?

是的,这在两种语言中都是合法的。

    test_ ## 0001 => test_0001             (identifier)
    test_0001 ## _THING => test_0001_THING (identifier)

    0001 ## _THING => 0001_THING           (pp-number)
    test_ ## 0001_THING => test_0001_THING (identifier)

无效令牌连接的示例有哪些?

假设我们有

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

现在,无论串联顺序如何,以下内容均无效:

concat3(., ., .)

.. 不是令牌,尽管 ... 是。但是连接必须按某种顺序进行,.. 将是一个必要的中间值;由于这不是单个标记,因此串联无效。

concat3(27,e,-7)

这里,-7是两个token,不能拼接。

这是一个连接顺序很重要的例子:

concat3(27e, -, 7)

如果这是从左到右连接的,它将导致 27e- ## 7,这是两个 pp 数字的连接。但是 - 不能与 7 连接,因为(如上)-7 不是单个标记。

pp-number到底是什么?

一般而言,pp-number 是标记的超集,可能会转换为(单个)数字文字或可能无效。该定义有意宽泛,部分是为了允许(某些)标记连接,部分是为了使预处理器免受数字格式的周期性变化的影响。准确的定义可以在各自的标准中找到,但通俗地讲,令牌是 pp-number if:

  • 它以十进制数字或句点 (.) 开头,后跟十进制数字。
  • 令牌的其余部分是字母、数字和句点,如果前面有指数符号,则可能包括符号字符(+-)。两种语言的指数符号都可以是Ee;以及自 C99 以来 C 中的 Pp
  • 在 C++ 中,pp-number 还可以包含(但不是开头)撇号后跟字母或数字。
  • 注:以上,letter包含下划线。此外,可以使用通用字符名称(C++ 中的撇号除外)。

一旦预处理终止,所有 pp-numbers 将尽可能转换为数字文字。如果无法转换(因为令牌不对应于任何数字文字的语法),则该程序无效。

#define test(A) test_## A ## _THING
int test(0001) = 2;

这对于 LTR 和 RTL 评估都是合法的,因为 test_00010001_THING 都是有效的 preprocessor-tokens。前者是一个标识符,而后者是一个pp-number; pp-number 直到编译的后期阶段才会检查后缀的正确性;想例如0001u 一个无符号八进制文字。

几个例子表明求值顺序确实重要:

#define paste2(a,b) a##b
#define paste(a,b) paste2(a,b)
#if defined(LTR)
#define paste3(a,b,c) paste(paste(a,b),c)
#elif defined(RTL)
#define paste3(a,b,c) paste(a,paste(b,c))
#else
#define paste3(a,b,c)  a##b##c
#endif
double a = paste3(1,.,e3), b = paste3(1e,+,3);  // OK LTR, invalid RTL

#define stringify2(x) #x
#define stringify(x) stringify2(x)
#define stringify_paste3(a,b,c) stringify(paste3(a,b,c))
char s[] = stringify_paste3(%:,%,:);            // invalid LTR, OK RTL

如果您的编译器使用一致的求值顺序(LTR 或 RTL) 在生成无效 pp-token 时出现错误,那么正好是其中之一行会产生错误。自然地,宽松的编译器很可能允许两者,而严格的编译器可能两者都不允许。

第二个例子比较做作;由于语法的构造方式,很难找到在构建 RTL 时有效但在构建 LTR 时无效的 pp-token。

C 和 C++ 在这方面没有显着差异;这两个标准使用相同的语言(直到 headers 部分)来描述宏替换的过程。语言影响过程的唯一方式是在有效 preprocessing-tokens 中:C++(尤其是最近)有更多形式的有效 preprocessing-tokens,例如 user-defined 字符串文字。