C11 中对 Unicode 转义序列的限制

Restrictions to Unicode escape sequences in C11

为什么 C11 中对 Unicode 转义序列(\unnnn\Unnnnnnnn)有限制,只能表示基本字符集之外的字符?例如,以下代码会导致编译器错误:\u000A is not a valid universal character。 (一些 Unicode“词典”站点甚至将这种无效格式作为 C/C++ 语言的规范,尽管不可否认这些很可能是自动生成的):

static inline int test_unicode_single() {
        return strlen(u8"\u000A") > 1;
}

虽然我知道这些基本字符不一定需要受支持,但是否有技术原因不支持这些字符?比如不能用不止一种方式来表示同一个字符?

正是为了避免其他拼写。

向 C 和 C++ 添加通用字符名称 (UCN) 的主要动机是:

  • 允许标识符包含基本源字符集之外的字母(例如 ñ)。

  • 允许 table 编写字符串和字符文字的机制,其中包括基本源字符集之外的字符。

此外,希望对现有编译器的更改尽可能有限,特别是编译器(和其他工具)可以继续使用其已建立的(通常是高度优化的)词法分析功能。

这是一个挑战,因为不同编译器的词法分析架构存在巨大差异。无需深入了解所有细节,似乎有两种广泛的实施策略是可能的:

  1. 编译器可以在内部使用一些单一的通用编码,例如 UTF-8。其他编码的所有输入文件都将在输入管道的早期被转录成这种内部编码。此外,UCN(无论它们出现在哪里)都将被转换为相应的内部编码。后一种转换可以与续行处理并行进行,续行处理也需要检测反斜杠,从而避免对每个输入字符进行额外的测试,因为这种情况很少会被证明是真的。

  2. 编译器可以在内部使用严格的(7 位)ASCII。允许其他字符编码的输入文件将被转录为 ASCII,非 ASCII 字符将在任何其他词法分析之前转换为 UCN。

实际上,这两种策略都将在阶段 1(或等效阶段)中实施,这比词法分析发生早得多。但请注意区别:策略 1 将 UCN 转换为内部字符编码,而策略 2 将 non-representable 字符转换为 UCN。

这两种策略的共同点是,一旦转录完成,直接输入源流的字符(无论源文件使用何种编码)和用联合国教科文组织。因此,如果编译器允许 UTF-8 源文件,您可以输入 ñ 作为两个字节 0xc3、0xb1 或作为六字符序列 \u00D1,它们最终会相同字节序列。反过来,这意味着每个标识符只有一个拼写,因此无需更改(例如)符号 table 查找。

通常情况下,编译器只是通过编译管道传递变量名,让它们最终由汇编器或链接器处理。如果这些下游工具不接受扩展字符编码或 UCN(取决于实施策略),则包含此类字符的名称需要“损坏”(转录)才能使它们被接受table。但即使这是必要的,这也是一个微小的变化,可以在定义良好的界面上完成。

C 和 C++ 标准委员会没有解决其产品(或开发团队)在这两种策略之间有明确偏好的编译器供应商之间的争论,而是选择了使两种策略兼容的机制和限制。特别是,两个委员会都禁止使用代表已经在基本源字符集中编码的字符的 UCN。这避免了像这样的问题:

  • 如果我将 \u0022 放入字符串文字中会发生什么:

      const char* quote = "\u0022";
    

    如果编译器将 UCN 转换为它们所代表的字符,那么当词法分析器看到该行时,"\u0022" 将被转换为 """,这是一个词法错误。另一方面,将 UCN 保留到最后的编译器会很乐意将其作为字符串文字接受。禁止使用表示引号的 UCN 可避免这种可能的不可移植性。

  • 同理,'\u005cn'会不会是换行符?同样,如果 UCN 在阶段 1 中转换为反斜杠,那么在阶段 3 中字符串文字肯定会被视为换行符。但是,如果仅在字符文字标记被识别为字符文字标记后才将 UCN 转换为字符值,则生成的字符文字将包含两个字符(实现定义的值)。

  • 那么 2 \u002B 2 呢?即使 UCN 不应该用于标点符号,这看起来像是一个补充吗?或者它看起来像一个以非字母代码开头的标识符?

等等,针对大量类似问题。

通过要求 UCN 不能用于拼写基本源字符集中的字符的简单权宜之计,避免了所有这些细节。这就是标准所体现的内容。

请注意,“基本源字符集”并不包含所有 ASCII 字符。它不包含大多数控制字符,也不包含 ASCII 字符 $@`。这些字符(在字符串和字符文字之外的 C 或 C++ 程序中没有意义)可以分别写为 UCN \u0024\u0040\u0060

最后,为了了解您需要解开什么样的结才能正确地对 C(或 C++)进行词法分析,请考虑以下代码段:

const char* s = "\
n";

因为连续行在阶段 1 中处理,在词法分析之前,阶段 1 只查找由反斜杠和换行符组成的双字符序列,该行与

const char* s = "\n";

但从原始代码来看,这可能并不明显。