C11 和 C++11 扩展和通用字符转义

C11 & C++11 Exended and Universal Character Escaping

本文关键字:字符 转义 扩展 C++11 C11      更新时间:2023-10-16

上下文

C11和C++11都支持源文件中的扩展字符,以及通用字符名(UCN),这允许用户只使用基本源字符集中的字符输入字符。

C++11还定义了编译的几个翻译阶段。特别是,在翻译的第一阶段,扩展字符被标准化为UCN,如下所述:

§C++11 2.2p1.1:

在实现定义的方式,到基本源字符集(为行尾指示器引入换行符)如果必需的接受的物理源文件字符集为实现定义。将三角图序列(2.4)替换为相应的单字符内部表示。任何来源不在基本源字符集(2.3)中的文件字符被替换通过指定该字符的通用字符名。(安实现可以使用任何内部编码,只要实际在源文件中遇到的扩展字符在源文件中表示为通用字符名(即使用\uXXXX表示法)等效处理,除非在原始字符串文字。)


问题

是否编制符合标准的程序

#include <stdio.h>
int main(void){
printf("én");
printf("\u00e9n");
return 0;
}

失败,编译并打印

é
é

或编译和打印

u00e9
u00e9

,何时运行?


知情的个人意见

我的论点是,答案是它成功地编译和打印了u00e9,因为根据上面的§2.2p1.1,我们有

一个实现可以使用任何内部编码,只要在源文件中遇到实际的扩展字符,并且源文件中表示的相同扩展字符为通用字符名(即使用\uXXXX表示法),被等效地处理,除非在原始字符串文本中还原此替换,并且我们不在原始字符串文字中。

然后,

  • 在阶段1中,printf("én");映射到printf("\u00e9n");
  • 在第3阶段,源文件被分解为预处理标记(§2.2p1.3),其中字符串文本"\u00e9n"就是其中之一
  • 在第5阶段,字符文字或字符串文字中的每个源字符集成员,以及字符文字或非原始字符串文字中每个转义序列和通用字符名,都被转换为执行字符集的相应成员(§2.2p1.5)。因此,根据最大咀嚼原则,\映射到,并且片段CCD_ 7不被识别为UCN并且因此按原样打印

实验

不幸的是,现存的编译器不同意我的观点。我已经用GCC 4.8.2和Clang 3.5进行了测试,下面是他们给我的:

  • GCC 4.8.2

    g++ -std=c++11  -Wall -Wextra ucn.cpp -o ucn
    

    输出:

    ucn.cpp: In function 'int main()':
    ucn.cpp:4:9: warning: unknown escape sequence: '303' [enabled by default]
    printf("én");
    ^
    
    出:
    é
    u00e9
    
  • Clang 3.5

    clang++ -std=c++11  -Wall -Wextra ucn.cpp -o ucn
    

    输出:

    ucn.cpp:4:10: warning: unknown escape sequence 'xFFFFFFC3' [-Wunknown-escape-sequence]
    printf("én");
    ^
    ucn.cpp:4:12: warning: illegal character encoding in string literal [-Winvalid-source-encoding]
    printf("én");
    ^
    2 warnings generated.
    
    出:
    é
    u00e9
    

我使用hexdump -C ucn.cpp检查了é字符是否显示为C3 A9,这与预期的UTF-8编码一致。此外,我已经验证了普通的printf("én");printf("u00e9n");可以完美地工作,所以这不是被测试的编译器无法读取UTF-8源文件的问题。

谁是对的

"é"不是字符串文字中反斜杠转义的有效字符,因此反斜杠后面跟着"é"作为文字源字符或UCN应该会产生编译器诊断和未定义的行为。

但是,请注意,"\u00e9"不是一个前面有反斜杠的UCN,并且不可能在字符串或后面有反斜杠UCN的字符文字中写入任何基本源字符序列。因此,"é""\u00e9"的行为不需要相同:"\u00e9"的行为可以很好地定义,而"é"的行为是未定义的。

如果我们假设一些语法允许反斜杠转义UCN,比如"«u00e9»",那么它将具有像"é"这样的未定义行为。


  • 在阶段1中,printf("én");映射到printf("\u00e9n");

é到UCN的第一阶段转换无法创建非UCN,例如"\u00e9"


编译器是对的,但没有用完美的诊断消息专门处理这种情况。理想情况下,你会得到:

$ clang++ -std=c++11  -Wall -Wextra ucn.cpp -o ucn
ucn.cpp:4:10: warning: unknown escape sequence 'é' [-Wunknown-escape-sequence]
printf("én");
^
1 warnings generated.
$ ./ucn
é
u00e9

两个编译器都规定,在存在未知转义序列的情况下,它们的行为是用这样转义的字符替换转义序列,因此"é"将被视为"é",整个程序应被解释为:

#include <stdio.h>
int main(void){
printf("én");
printf("\u00e9n");
return 0;
}

两个编译器都会出现这种行为,部分是偶然的,但部分是因为以这种方式处理未识别的转义序列的策略是一个明智的选择:即使他们只将未识别的escape序列视为后面跟着字节0xC3的反斜杠,他们也会删除反斜杠并保留0xC3,这意味着UTF-8序列保持不变以供以后处理。

您似乎很困惑,认为\u00e9是一个UCN——事实并非如此。UCN都以u开头,在您的情况下,您有一个提取反斜杠,它可以转义这个初始反斜杠。因此,\u00e9是由6个字符组成的序列:u00e9

编辑

在第1阶段,printf("\é\n");映射到printf("\u00e9\n");。

这就是问题所在——阶段1将输入字符转换为源字符,因此printf("én");映射到printf("én");,它与printf("u00e9n");相同,但由于后者中的双反斜杠,这与printf("\u00e9n");映射的内容不同。由于对双反斜杠的特殊处理,在源中不可能有一个反斜杠后面跟着一个UCN。