【问题标题】:C11 & C++11 Exended and Universal Character EscapingC11 & C++11 扩展和通用字符转义
【发布时间】:2015-07-21 03:28:28
【问题描述】:

上下文

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("\\u00e9\n");
        return 0;
}

失败,编译并打印

é
é

或编译打印

\u00e9
\u00e9

,什么时候运行?


知情的个人意见

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

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

接下来就是

  • 在第 1 阶段,printf("\é\n"); 映射到 printf("\\u00e9\n");
  • 在第 3 阶段,源文件被分解为预处理标记(第 2.2p1.3 节),其中 string-literal"\\u00e9\n" 是其中之一。
  • 在第 5 阶段,字符文字或字符串文字中的每个源字符集成员,以及字符文字或非原始字符串文字中的每个转义序列和通用字符名称都会被转换到执行字符集的相应成员 (§2.2p1.5)。因此,根据最大咀嚼原则,\\ 映射到 \,并且片段 u00e9 不被识别为 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");
             ^
    $ ./ucn
    é
    \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.
    $ ./ucn
    é
    \u00e9
    

我使用hexdump -C ucn.cpp 双重和三重检查é 字符是否显示为C3 A9,与预期的UTF-8 编码一致。此外,我还验证了普通的 printf("é\n");printf("\u00e9\n"); 可以完美运行,因此这不是测试的编译器无法读取 UTF-8 源文件的问题。

谁是对的?

【问题讨论】:

  • N3881 讨论了这个问题,但我不知道委员会采取了任何行动来解决这个问题。
  • @cpplearner 出色的侦探工作;这正是我想要阅读的那种文件。如果 cmets 是最受欢迎的,我会为你的。

标签: c++ c c++11 language-lawyer c11


【解决方案1】:

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

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

如果我们要设置一些允许反斜杠转义 UCN 的语法,例如 "\«\u00e9»",那么就会有未定义的行为,例如 "\é"


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

é 到 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("\\u00e9\n");
        return 0;
}

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

【讨论】:

  • 这让我对你的论点感到困惑:é 到 UCN 的第一阶段转换不能创建非 UCN,例如 "\\u00e9"。到阶段 1 结束时,"é""\u00e9" 之间没有区别在编译器中,因为两者都被理解为"\u00e9"。从那时起,编译器必须对两者给予完全相同的处理,因为它们是相同的。第 1 阶段发生在在字符串/转义序列的概念被理解之前,因此没有理由认为它可以区别对待\\u00e9。每当出现é 时,第 1 阶段就会直接粘贴到 \u00e9
  • 在任何情况下,§2.2p1.1 和许多其他地方都声明源文件中的扩展字符被处理相同它被替换为\uXXXX 符号在源文件中 中的UCN。在阶段 2+ 中,编译器无法将 "\\u00e9" 中的 \u00e9 单独作为一个字符来隔离,因为它源自 é,原因很简单,从这些后续阶段的角度来看,程序员实际上确实输入了 \\u009 - 在源文件中
  • "到阶段 1 结束时,编译器中“é”和 "\u00e9" 之间没有区别,但是组成 UCN 的字符“\u00e9”和字符“之间存在区别\u00e9" 不构成 UCN。
  • 规范声明扩展字符被 UCN 替换:但这与被替换为可能看起来像 UCN 但不是的字符序列不同,因为前面的反斜杠。没有任何内容表明“é”被无法构成 UCN 的字符序列所取代。
  • 这不是它所说的。它不是简单地引用以'\''u' 开头的字符序列,而是引用语法元素universal-character-name。这个元素没有有效的方式出现在这个位置,你不能仅仅将它重新解释为将语法元素分解为源字符,然后重新解析为不同的语法元素。结果是规范没有定义任何行为的情况,这意味着你得到未定义的行为
【解决方案2】:

您似乎很困惑,认为\\u00e9 是一个UCN——它不是。 UCN 都以\u 开头,在您的情况下,您有一个提取反斜杠,它转义了这个初始反斜杠。所以\\u00e9是6个字符的序列:\u00e9

编辑

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

这是你出错的地方——阶段 1 将输入字符转换为源字符,所以 printf("\é\n"); 映射到 p r i n n t f ( " 987654339 é 987654341 n 987654343 ) 987654345 @,这是相同的p 987654347 i 987654349 t 987654351 ( " \ \u00e9 \ n " ) ;,但这与 printf("\\u00e9\n"); 映射的内容不同,因为后者中有双反斜杠。由于双反斜杠的特殊处理,源代码中无法在反斜杠后跟 UCN。

【讨论】:

  • 在这一点上,我确定我没有感到困惑;我绝对知道\\u00e9 分解为这 6 个字符。问题是 是否也转换为这 6 个字符(通过我对标准的阅读)或只是 é,如果是,为什么。 GCC 和 Clang 似乎在这里不遵守标准的字母,所以自然会产生为什么(也许我错过了一部分?)的问题。更一般地说,它是关于 UCN 和扩展字符在翻译中的确切位置。
  • 他们并没有违反这里的标准——只是没有像您假设的那样从内部字符转换回源字符。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-06-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-07-08
相关资源
最近更新 更多