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

时间:2015-07-09 14:29:33

标签: c++ c c-preprocessor c89

我想广泛使用## - 运算符和enum magic来处理大量类似的访问操作,错误处理和数据流。

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

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

所以我的问题是:

  1. 有效pp-tokens有哪些规则?

  2. 不同的C和C ++标准之间是否存在差异?

  3. 我当前的问题:以下所有2个运营商订单是否合法?(**)

    #define test(A) test_## A ## _THING
    int test(0001) = 2;
    
  4. 评论:

    (*)我不使用“未定义”,因为这与未定义的行为有关,但恕我直言,而是未指定的行为。应用了多个##或#运算符通常不会使程序错误。显然有一个订单 - 我们无法预测哪个 - 因此订单未指定。

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

2 个答案:

答案 0 :(得分:5)

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

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

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

但是,在##运算符的上下文中,连接的结果必须是单个预处理标记。因此,无效串联是一个串联,其结果是一系列包含多个预处理标记的字符。

C和C ++之间是否存在差异?

是的,存在细微差别:

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

  • C ++包含符号::.*->*,所有符号都将被标记为C中的两个标点符号标记。此外,在C ++中,一些看似关键字的东西(例如newdelete)是 preprocessing-op-or-punc 的一部分(尽管这些符号是有效的两种语言的预处理令牌。)

  • C允许使用十六进制浮点文字(例如1.1p-3),它们在C ++中无效预处理标记

  • 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是两个令牌,因此无法连接。

以下是连接顺序很重要的情况:

concat3(27e, -, 7)

如果这是从左到右连接的,则会产生27e- ## 7,这是两个pp数的串联。但-无法与7连接,因为(如上所述)-7不是单个令牌。

pp-number 究竟是什么?

一般而言, pp-number 是令牌的超集,可能会转换为(单个)数字文字或可能无效。该定义有意宽泛,部分是为了允许(某些)令牌连接,部分是为了使预处理器与数字格式的周期性变化隔离。精确定义可以在相应的标准中找到,但非正式地,令牌是 pp-number ,如果:

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

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

答案 1 :(得分:0)

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

这对LTR和RTL评估都是合法的,因为test_00010001_THING都是有效的预处理器令牌。前者是标识符,后者是pp号;在编译的后期阶段,不检查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时有效的pp-token,但在构建LTR时却没有。

在这方面,C和C ++之间没有显着差异;这两个标准具有相同的语言(直到节标题),描述了宏替换的过程。语言影响进程的唯一方法是在有效的预处理标记中:C ++(特别是最近)有更多形式的有效预处理标记,例如用户定义的字符串文字。