假设我们给出了这个输入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代码:
#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能够生成对程序员有用的各种信息,但不能与标准中的任何内容相对应。例如,翻译的预处理器阶段也可以使用其中一个来生成对插入Makefile有用的依赖性信息 -M
选项。或者,可以使用输出编译代码的人类可读版本 -S
选项。并且可以使用以下方式输出预处理程序的可编译版本(大致对应于阶段4生成的令牌流)。 -E
选项。这些输出格式都不受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 -x c - <<<'
#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 <stdio.h>
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中传递的令牌流中,令牌 int
和 main
没有空格分隔(也没有 return
和 puts
)。字符串化清楚地显示了这一点,其中没有空格分隔令牌。但是,即使明确传递,程序也会编译并执行正常 gcc -E
:
$ gcc -E squish.c | gcc -x c - && ./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_NUMBER标记(rici称为“pp-number”)和右侧的“+”标记进行调用。在这种情况下,它无条件地说“是的,你需要插入一个空格以避免粘贴”,而不是检查数字标记的最后一个字符是否是eEpP之一。
编译器设计通常归结为准确性和实现简单性之间的权衡。