假设我们给出了这个输入C代码:
#define Y 20
#define A(x) (10+x+Y)
A(A(40))
Run Code Online (Sandbox Code Playgroud)
gcc -E这样的输出(10+(10+40 +20)+20).
gcc -E -traditional-cpp这样的输出(10+(10+40+20)+20).
为什么默认的cpp会在之后插入空格40?
我在哪里可以找到涵盖该逻辑的最详细的cpp规范?
ric*_*ici 10
C标准没有指定这种行为,因为预处理阶段的输出只是一个令牌和空格流.将标记流序列化为字符串,这是gcc -E标准所不需要或甚至没有提到的字符串,并且不构成标准指定的翻译过程的一部分.
在阶段3中,程序"被分解为预处理标记和空白字符序列".除了忽略空格的连接运算符和保留空格的字符串化运算符的结果之外,还会修复标记,并且不再需要空格来分隔它们.但是,需要空格以便:
直到阶段7,流中的空白元素才被消除,尽管在阶段4结束后它们不再相关.
Gcc能够生成对程序员有用的各种信息,但不能与标准中的任何内容相对应.例如,转换的预处理器阶段还可以使用其中一个-M选项生成对插入Makefile有用的依赖性信息.或者,可以使用该-S选项输出编译代码的人类可读版本.并且可以使用该-E选项输出预处理程序的可编译版本,其大致对应于阶段4产生的令牌流.这些输出格式都不受C标准的任何控制,C标准仅涉及实际执行程序.
为了产生-E输出,gcc必须以不改变程序语义的格式序列化令牌流和空格流.如果它们没有彼此分离,则存在流中的两个连续令牌被错误地粘合在一起成为单个令牌的情况,因此gcc必须采取一些预防措施.它实际上不能将空格插入正在处理的流中,但是当它呈现流响应时,没有什么能阻止它添加空格gcc -E.
例如,如果示例中的宏调用被修改为
A(A(0x40E))
Run Code Online (Sandbox Code Playgroud)
那么令牌流的天真输出将导致
(10+(10+0x40E+20)+20)
Run Code Online (Sandbox Code Playgroud)
因为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)"
Run Code Online (Sandbox Code Playgroud)
字符串化中的空白处理由标准精确指定:(§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));}
Run Code Online (Sandbox Code Playgroud)
这是gcc -E的输出(最后的好东西):
$ gcc -E squish.c | tail -n2
char*quoted="intmain(void){returnputs(quoted);}";
int main(void){return puts(quoted);}
Run Code Online (Sandbox Code Playgroud)
在从阶段4传出的令牌流中,令牌int并main没有被空格分隔(并且都不是return和puts).字符串化清楚地表明了这一点,其中没有空格分隔令牌.但是,即使通过以下方式明确传递,程序也会编译并执行正常gcc -E:
$ gcc -E squish.c | gcc -x c - && ./a.out
intmain(void){returnputs(quoted);}
Run Code Online (Sandbox Code Playgroud)
并编译输出gcc -E.
不同的编译器和相同编译器的不同版本可以产生预处理程序的不同序列化.所以我认为你不会发现任何可以通过逐个字符与-E给定编译器的输出进行比较来测试的算法.
最简单的序列化算法是无条件地在两个连续令牌之间输出空格.显然,这将输出不必要的空格,但它永远不会在语法上改变程序.
我认为最小空间算法是在令牌中最后一个字符的末尾记录DFA状态,以便稍后如果存在从第一个令牌末尾的状态转换,则可以在两个连续令牌之间输出空格.在下一个标记的第一个字符上.(将DFA状态保持为令牌的一部分与将令牌类型保持为令牌的一部分本质上没有区别,因为您可以从DFA状态的简单查找中派生令牌类型.)该算法不会在之后插入空格40在你的原始测试用例中,但它会在之后插入一个空格0x40E.因此,您的gcc版本不使用该算法.
如果使用上述算法,则需要重新扫描由标记串联创建的标记.但是,无论如何,这是必要的,因为如果连接的结果不是有效的预处理标记,则需要标记错误.
如果您不想记录状态(尽管如我所说,这样做基本上没有成本)并且您不想通过在输出时重新扫描令牌来重新生成状态(这也很便宜) ),您可以预先计算由令牌类型和后续字符键入的二维布尔数组.计算基本上与上述相同:对于每个接受返回特定标记类型的DFA状态,在该标记类型的数组中输入一个真值,以及任何转换超出DFA状态的字符.然后,您可以查找令牌的令牌类型和以下令牌的第一个字符,以查看是否需要空格.此算法不会产生最小间距的输出:例如,它会40在您的示例中放置一个空格,因为它是40a,pp-number并且有些可能pp-number会扩展为a +(即使您不能40以这种方式扩展) .因此gcc可能会使用此算法的某些版本.
| 归档时间: |
|
| 查看次数: |
1369 次 |
| 最近记录: |