问题 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规范?


1865
2018-06-13 18:40


起源

出于兴趣 - 为什么这对你很重要? - Soren
将我自己的实现与gcc进行比较。 - alinsoar
@alinsoar你可以lex两个输出并比较结果的令牌序列。 - Niklas B.
@JohnBollinger:但是在替换列表中,当前被替换的宏的名称不符合替换条件。从这个意义上讲,扩展不能嵌套。 - rici
@WeatherVane:对,但是 A(40) 没有完全扩大 与替换列表 第一个 A;它在插入之前完全展开。对比 #define A(x,y) x y  A(A,(40,60)) 在哪里扩张 A(40,60)不会发生,因为它是在重新扫描的上下文中。 - rici


答案:


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可能会使用这个算法的某个版本。


10
2018-06-13 18:51



我会小心的。 C标准非常清楚如何在预处理阶段处理输入。 IIRC,它还要求在某些点插入中间空间。 - too honest for this site
为什么你认为gnu开发人员努力插入空格 - 这是重点。我从他们想要纠正上层处理级别中的一些可能的错误的想法开始,等等。 - alinsoar
@alinsoar:我认为这是为了避免意外输出两个可能粘在一起的连续令牌。例如,如果 x 曾经 0x1e,那么空间是必要的,以便能够往返。但在内部,它只是一系列令牌。 - rici
好的,您是否找到了有关此问题的相关文档? - alinsoar
@rici,我不能把它说得更清楚,除了字符串化。该标准在这里是特定的:“参数的预处理标记之间每次出现的空格都成为字符串文字中的单个空格字符。第一个预处理标记之前和构成参数的最后一个预处理标记之后的空格被删除。”请注意,这确实区分了由空格分隔的预处理标记和非空格分隔的预处理标记。 - John Bollinger


为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之一。

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


1
2018-06-26 14:45



很漂亮,谢谢! - alinsoar
关于预处理器,我现在感兴趣的是关于它如何为常量lexemes附加类型的完整算法 - 例如int for 'a',wchar_t for L'a',unsigned char for '\x22' 您能否建议我在哪里查看gcc如何将类型附加到输入常量?是否有关于gcc类型如何检查C代码的详细文档? - alinsoar
没有这样的文档,据我所知,也没有交叉引用的源代码在线副本。但是开始阅读代码的地方是 gcc/c-family/c-lex.c,追逐函数调用 libcpp。 - zwol
你的问题正在接近如此GCC特定,以至于他们在这里得不到好的答案,因为没有人知道足够细节的内脏(我以前知道很多,但那是10年前);但如果概括,“C编译器如何决定数字文字的类型”?可能会被“读一本关于C的好书”解雇,因为该算法在C标准中有详细说明。我建议你把这些问题带到 gcc@gcc.gnu.org 邮件列表而不是。要非常清楚,您正在尝试阅读和理解代码。 - zwol
如果你不知道如何深入研究一个令人困惑的难以记录的源代码树,现在是时候学习了。唉,唯一的学习方法就是做。我会提供建议,但它不适合评论框,甚至答案框的范围。同样,您对路线图的最佳选择是与实际仍参与GCC开发的人员交谈。 - zwol