欢迎光临散文网 会员登陆 & 注册

【C/C++】给编译器搅局的词法 - 原始字符串

2023-04-04 19:07 作者:冒-_-泡  | 我要投稿

前文提到,翻译阶段3是面向预处理的词法分析,虽然和正式编译的词法分析,或者说在编译原理中提到的有一些区别,但也大差不差,基本目的就是将字符序列分割成一个个词(也称token),当然光确定具体的分割点还不够,重要的是确定每个词的含义和内容

在cpprefrence的阶段3描述中,有这么一条:

撤回在任何原始字符串字面量的首尾双引号之间在阶段 1 和 2 期间进行的所有变换。

(C++11 起)

原始字符串是C++11新增的语法(或者确切说是词法),不考虑其他前缀的话,格式是:

和Python的原始字符串类似,两边的括号和d字符序列是用来指定开始结束的,通过自定义特别的开始结束标记,在中间就可以安全使用双引号之类字符了,原则上说,原始字符串表示的是代码中原始的内容,支持跨行,如上面的例子,表示“\”、“n”和换行这三个字符,具体细节语法书有详细说明,就不细说了

一般来说,编译流程是一步步前进的,即便有回退,也是局部性的(比如语法分析中的一些匹配尝试),那为什么阶段3会有上面这条撤回的奇怪规定呢,举个例子说明:

对于这个代码中的原始字符串,根据前面对翻译阶段1和2的解释,我们知道编译器会在阶段1进行字符替换,三标符“??/”被替换为反斜杠(注:为举例方便,下面都默认开启三标符语法),而到了阶段2,会进行行尾反斜杠的行拼接处理,这样这个字符串就被改成空串了。类似的情况也会出现在阶段1需要处理的\u和\U通用字符转义的处理中

但是对一般的程序员来说,写出这种代码是为了表示什么?从标准角度,我们知道三标符和通用符的处理隶属于阶段1,而并不是\r、\n、\ooo、\xHH之类的字符转义,后者从概念上是隶属于字符和字符串字面量,在阶段5处理。但是,这种认知对于一般人来说过于复杂,而且过于接近底层实现了,更何况三标符这种属于历史原因的遗留

所以对于原始字符串来说,C++11将其设计为内容和代码中完全一致(注:原则上,下面有特例说明),无视阶段1和2的处理,这在理解上会简单很多,而且能规避一些麻烦的情形,比方说,如果阶段1和2的处理被接纳进来,用原始字符串表示原始的"??="这三个字符就会很麻烦,至少你不能写R"x(?\?=)x",因为就算绕过了阶段1,“\?”的转义在这里也是无效的,凭空多了一个反斜杠了

于是,由于阶段3是词法处理,需要识别出字符串这一类token,标准就在这里要求对原始字符串撤回阶段1和2的任何改动了,将内容改回代码中的样式

为什么不能在一开始就规避,例如规定在阶段1中,对原始字符串中的三标符和通用符不做处理呢?答:因为没法识别,字符串token必须在词法阶段才可以识别,阶段1和2只是面向字符序列的简单文本处理

这个规定给编译器实现带来了一个不大不小的麻烦,这个需求说大也算不上大,但是实际处理起来又挺繁琐的,比方说,我们可以记录下阶段1和2所有替换的位置和原始内容,从而方便实现回滚,但如果只记录偏移位置,在回滚替换时又得注意偏移之间的影响等等

不过根据之前视频讲过的AS-IF原则,如果用户不关心阶段1和2的处理结果(一般需要关注的最早的代码中间结果是预处理之后,即阶段4的),那么编译器也可以将阶段123合并成一个流程来处理,即一边进行字符和行处理,一边进行词法分析,如此在后者解析到原始字符串内容时,skip掉前者的逻辑就行了

根据以上的论述,我们是不是就可以认为:原始字符串表示的是代码中原始的内容?其实我上面还加了个前提:“原则上说”,看这个例子:

注意,这里采用了vim中的表示法,“^M”在这里表示“\r”这个字符,而不是“^”和“M”

我们知道不同平台的换行是有些不同的,微软的系统用“\r\n”,而POSIX系统(如*nix、Mac)是“\n”,一般而言,对于C和C++,这个差别不会有什么影响,因为代码是换行和空白符不敏感的,除了单行注释和预处理命令等特殊语法外,你把所有代码写到一行都没有关系,在原始字符串出现之前,行末有没有\r关系不大,虽然\r并非C的基础字符集,编译器还是将其视为一个空白符(例如当做空格或换行)

但是在原始字符串中,如果出现了换行是\r\n,或\r单独出现的情形,那情况就要复杂了,由于\r不是C代码的基础字符集,所以在代码文件被编译器读入后,转换为什么是一个实现定义行为,例如以上代码,在我这边的linux环境下,用gcc和clang测试:

看上去这俩编译器对于行尾的\r\n都转成了\n,而前面这个单独出现的\r,gcc将其转为\n,clang则保留为\r不变

如果说这种差别不是专门针对\r的,而是上述的“非基础字符集字符的转换按实现定义”导致,那问题就严重一点,因为除了基础字符集外,还有“@”、“$”和码值126以后的字节都在内,如果各编译器处理有差异,那字符串里面写汉字会不会有乱码的可能?

幸运的是现在还没发现在中文编码方面的问题,而且C++本来也有unicode字面量,测了一些case,好像就\r这里是有差异,具体规则我也没找到权威说明,只能说如果需要对字符串中每个字节有把控的需求,那使用原始字符串还是慎重一些,用普通字符串转义处理较为稳妥了

至于gcc将\r转为\n的行为是否违反了上面这条撤回的规则,可以这样理解:撤回处理是指将原始字符串内容部分撤回至翻译阶段开始的状态,而此时是出于将源代码从磁盘读入内存,且将字节序列转换为字符序列之后,即关于\r的处理并非阶段1,而是最早的文件读取阶段,这样就容易理解了,毕竟如果用fopen的文本模式打开文件的话,行末\r\n读出来就只是\n了,编译器此时甚至都感知不到\r的存在

【C/C++】给编译器搅局的词法 - 原始字符串的评论 (共 条)

分享到微博请遵守国家法律