C预处理器插入的空格

假设我们给出了这个输入C代码:

#define Y 20 #define A(x) (10+x+Y) A(A(40)) 

gcc -E输出就像那样(10+(10+40 +20)+20)

gcc -E -traditional-cpp输出(10+(10+40+20)+20)

为什么默认cpp在40之后插入空格?

我在哪里可以找到涵盖该逻辑的最详细的cpp规范?

C标准没有指定这种行为,因为预处理阶段的输出只是一个令牌和空格流。 将标记流序列化为字符串,这是gcc -E所做的,标准不需要甚至不提及,也不构成标准指定的翻译过程的一部分。

在阶段3中,程序“被分解为预处理标记和空白字符序列”。 除了忽略空格的连接运算符的结果,以及保留空格的字符串化运算符之外,还会修复标记,并且不再需要空格来分隔它们。 但是,需要空格以便:

  • 解析预处理程序指令
  • 正确处理字符串化运算符

直到阶段7,流中的空白元素才被消除,尽管在阶段4结束后它们不再相关。

Gcc能够生成对程序员有用的各种信息,但不能与标准中的任何内容相对应。 例如,转换的预处理器阶段还可以使用-M选项之一生成对插入Makefile有用的依赖性信息。 或者,可以使用-S选项输出编译代码的人类可读版本。 并且可以使用-E选项输出预处理程序的可编译版本,其大致对应于阶段4产生的令牌流。 这些输出格式都不受C标准的任何控制,C标准仅涉及实际执行程序。

为了产生-E输出,gcc必须以不改变程序语义的格式序列化令牌流和空格流。 如果它们没有彼此分离,则存在流中的两个连续令牌被错误地粘合到一个令牌中的情况,因此gcc必须采取一些预防措施。 它实际上不能将空格插入正在处理的流中,但是当它呈现流以响应gcc -E时,没有什么能阻止它添加空格。

例如,如果示例中的宏调用被修改为

 A(A(0x40E)) 

那么令牌流的天真输出将导致

 (10+(10+0x40E+20)+20) 

因为0x40E+20是一个无法转换为数字标记的单个pp-number标记,所以无法编译。 +之前的空格可以防止这种情况发生。

如果您尝试将预处理器实现为某种字符串转换,那么无疑会遇到严重问题。 正确的实现策略是首先标记化,如标准中所示,然后在标记和空白流上执行阶段4作为函数。

字符串化是一个特别有趣的情况,其中空格会影响语义,它可以用来查看实际令牌流的外观。 如果对A(A(40))的扩展进行字符串化,则可以看到实际没有插入空格:

 $ gcc -E -xc - <<<' #define Y 20 #define A(x) (10+x+Y) #define Q_(x) #x #define Q(x) Q_(x) Q(A(A(40)))' "(10+(10+40+20)+20)" 

字符串化中的空白处理由标准精确指定:(§6.10.3.2,第2段,非常感谢John Bollinger查找规范。)

参数的预处理标记之间每次出现的空格都会成为字符串文字中的单个空格字符。 第一个预处理标记之前和构成参数的最后一个预处理标记之后的空格被删除。

这是一个更精细的示例,其中gcc -E输出中需要额外的空格,但实际上并未插入到令牌流中(通过使用字符串化再次显示以生成真实令牌流。) I (标识)宏用于允许将两个令牌插入令牌流中,而不插入空格; 如果你想使用宏来组成#include指令的参数(不推荐,但可以这样做),这是一个有用的技巧。

也许这对您的预处理器来说可能是一个有用的测试用例:

 #define Q_(x) #x #define Q(x) Q_(x) #define I(x) x #define C(x,...) x(__VA_ARGS__) // Uncomment the following line to run the program //#include  char*quoted=Q(C(I(int)I(main),void){I(return)I(C(puts,quoted));}); C(I(int)I(main),void){I(return)I(C(puts,quoted));} 

这是gcc -E的输出(最后的好东西):

 $ gcc -E squish.c | tail -n2 char*quoted="intmain(void){returnputs(quoted);}"; int main(void){return puts(quoted);} 

在传递出阶段4的令牌流中,令牌intmain不用空格分隔(并且都不是returnputs )。 字符串化清楚地显示了这一点,其中没有空格分隔令牌。 但是,即使通过gcc -E显式传递,程序也会编译并执行正常:

 $ gcc -E squish.c | gcc -xc - && ./a.out intmain(void){returnputs(quoted);} 

并编译gcc -E的输出。


不同的编译器和相同编译器的不同版本可以产生预处理程序的不同序列化。 所以我认为你不会发现任何可以通过逐个字符与给定编译器的-E输出进行比较来测试的算法。

最简单的序列化算法是无条件地在两个连续令牌之间输出空格。 显然,这将输出不必要的空格,但它永远不会在语法上改变程序。

我认为最小空间算法是在令牌中最后一个字符的末尾记录DFA状态,以便稍后如果存在从第一个令牌末尾的状态转换,则可以在两个连续令牌之间输出空格。在下一个标记的第一个字符上。 (将DFA状态保持为令牌的一部分与将令牌类型保持为令牌的一部分本质上没有区别,因为您可以从DFA状态的简单查找中派生令牌类型。)该算法不会在之后插入空格在原始测试用例中为40 ,但它会在0x40E之后插入一个空格。 因此,您的gcc版本不使用该算法。

如果使用上述算法,则需要重新扫描由标记串联创建的标记。 但是,无论如何,这是必要的,因为如果连接的结果不是有效的预处理标记,则需要标记错误。

如果您不想记录状态(尽管如我所说,这样做基本上没有成本)并且您不希望通过在输出时重新扫描令牌来重新生成状态(这也很便宜) ),您可以预先计算由令牌类型和后续字符键入的二维布尔数组。 计算基本上与上述相同:对于每个接受返回特定标记类型的DFA状态,在该标记类型的数组中输入一个真值,以及任何转换超出DFA状态的字符。 然后,您可以查找令牌的令牌类型和以下令牌的第一个字符,以查看是否需要空格。 这个算法不会产生最小间距的输出:例如,它会在你的例子中在40之后放一个空格,因为40是一个pp-number并且有些pp-number可以用+扩展(即使你不能以这种方式扩展40 )。 所以gcc可能会使用这个算法的某个版本。

为rici的优秀答案添加一些历史背景。

如果您可以获得gcc 2.7.2.3的工作副本,请试用其预处理器。 那时预处理器是一个独立于编译器的程序,它使用了一种非常天真的文本序列化算法,它往往会插入比必要的空间更多的空间。 当Neil Booth,Per Bothner和我实现了集成预处理器(出现在gcc 3.0及之后)时,我们决定让-E输出同时更智能,但不会使实现过于复杂。 该算法的核心是库函数cpp_avoid_paste ,在https://gcc.gnu.org/git/?p=gcc.git;a=blob;f=libcpp/lex.c#l2990中定义,其调用者是这里: https : //gcc.gnu.org/git/? p = gcc.git; a = blob; f = gcc / c-family / c -ppoutput.c#l177(寻找“输出空间的微妙逻辑” ……“)。

就你的例子而言

 #define Y 20 #define A(x) (10+x+Y) A(A(40)) 

cpp_avoid_paste将在cpp_avoid_paste使用CPP_NUMBER标记(rici称为“pp-number”)和右侧的“+”标记进行调用。 在这种情况下,它无条件地说“是的,你需要插入一个空格以避免粘贴”,而不是检查数字标记的最后一个字符是否是eEpP之一。

编译器设计通常归结为准确性和实现简单性之间的权衡。