我想广泛使用## - 运算符和enum magic来处理大量类似的访问操作,错误处理和数据流。
如果应用##
和#
预处理程序运算符导致无效的pp-token,则行为在C中未定义。
一般情况下,预处理器操作的顺序未在C90中定义(*)(参见The token pasting operator)。现在在某些情况下,它发生(在不同来源,包括MISRA委员会,以及引用的页面),多个## /# - 运算符的顺序会影响未定义行为的发生。但我很难理解这些来源的例子,并确定了共同规则。
所以我的问题是:
有效pp-tokens有哪些规则?
不同的C和C ++标准之间是否存在差异?
我当前的问题:以下所有2个运营商订单是否合法?(**)
#define test(A) test_## A ## _THING
int test(0001) = 2;
评论:
(*)我不使用“未定义”,因为这与未定义的行为有关,但恕我直言,而是未指定的行为。应用了多个##或#运算符通常不会使程序错误。显然有一个订单 - 我们无法预测哪个 - 因此订单未指定。
(**)这不是编号的实际应用。但模式是等价的。
答案 0 :(得分:5)
这些在各自的标准中有详细说明; C11§6.4和C ++11§2.4。在这两种情况下,它们都对应于生产预处理令牌。除了 pp-number 之外,它们不应该太令人惊讶。剩下的可能性是标识符(包括关键字),“标点符号”(在C ++中, preprocessing-op-or-punc ),字符串和字符文字,以及任何不匹配的单个非空格字符任何其他生产。
除了少数例外,任何字符序列都可以分解为一系列有效的预处理标记。 (一个例外是无法匹配的引号和撇号:单引号或撇号不是有效的预处理标记,因此包含未终止的字符串或字符文字的文本无法标记化。)
但是,在##
运算符的上下文中,连接的结果必须是单个预处理标记。因此,无效串联是一个串联,其结果是一系列包含多个预处理标记的字符。
是的,存在细微差别:
C ++具有用户定义的字符串和字符文字,并允许“原始”字符串文字。这些文字将在C中以不同方式标记,因此它们可能是多个预处理标记或(在原始字符串文字的情况下)甚至无效的预处理标记。
C ++包含符号::
,.*
和->*
,所有符号都将被标记为C中的两个标点符号标记。此外,在C ++中,一些看似关键字的东西(例如new
,delete
)是 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 ,如果:
.
)开头,后跟十进制数字。+
,-
)。两种语言中的指数符号可以是E
或e
;自C99以来,C中还有P
和p
。letter
包含下划线。此外,可以使用通用字符名称(除了在C ++中使用撇号之外)。一旦预处理终止,所有 pp-numbers 将尽可能转换为数字文字。如果无法进行转换(因为令牌与任何数字文字的语法不对应),则程序无效。
答案 1 :(得分:0)
#define test(A) test_## A ## _THING
int test(0001) = 2;
这对LTR和RTL评估都是合法的,因为test_0001
和0001_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 ++(特别是最近)有更多形式的有效预处理标记,例如用户定义的字符串文字。