u8字面应该如何工作

How are u8-literals supposed to work?

本文关键字:工作 何工作 u8      更新时间:2023-10-16

难以理解 u8 字面的语义,或者更确切地说,理解 g++ 4.8.1 上的结果

这是我的期望:

const std::string utf8 = u8"åäö"; // or some other extended ASCII characters
assert( utf8.size() > 3);

这是 g++ 4.8.1 上的结果

const std::string utf8 = u8"åäö"; // or some other extended ASCII characters
assert( utf8.size() == 3);
  • 源文件是 ISO-8859(-1(
  • 我们使用这些编译器指令:-m64 -std=c++11 -pthread -O3 -fpic

在我的世界里,无论源文件的编码如何,生成的 utf8 字符串都应该长于 3。

或者,我是否完全误解了u8的语义及其针对的用例?请开导我。

更新

如果我像许多人建议的那样明确告诉编译器源文件的编码是什么,我得到了 u8 文字的预期行为。但是,常规文字也会被编码为 utf8

那是:

const std::string utf8 = u8"åäö"; // or some other extended ASCII characters
assert( utf8.size() > 3);
assert( utf8 == "åäö");
  • 编译器指令: g++ -m64 -std=c++11 -pthread -O3 -finput-charset=ISO8859-1
  • 尝试了从 iconv 定义的其他一些字符集,例如:ISO_8859-1 等...

我现在比以前更困惑了...

u8 前缀实际上只是意味着"编译此代码时,从此文字生成一个 UTF-8 字符串"。它没有说明编译器应如何解释源文件中的文本。

因此,您有几个因素在起作用:

  1. 哪个编码是写入的源文件(在您的情况下,显然是 ISO-8859(。根据此编码,字符串文字为"åäö"(3 个字节,包含值 0xc5、0xe4、0xf6(
  2. 编译器在读取源文件时采用哪种编码?(我怀疑 GCC 默认为 UTF-8,但我可能是错的。
  3. 编译器用于对象文件中生成的字符串的编码。您可以通过u8前缀将其指定为 UTF-8。

最有可能的是,#2 是出错的地方。如果编译器将源文件解释为 ISO-8859,那么它将读取这三个字符,将它们转换为 UTF-8,然后写入它们,结果为您提供一个 6 字节(我认为每个字符都编码为 2 字节的 UTF-8(字符串。

但是,如果它假设源文件是 UTF-8,那么它根本不需要进行转换:它读取 3 个字节,它假设是 UTF-8(即使它们是 UTF-8

的无效垃圾值(,并且由于您要求输出字符串也是 UTF-8,它只输出相同的 3 个字节。

你可以告诉GCC用-finput-charset采用哪种源编码,或者你可以将源编码为UTF-8,或者你可以在字符串文字中使用uXXXX转义序列(例如,u00E5而不是å(

编辑:

澄清一点,当您在源代码中指定带有u8前缀的字符串文本时,您是在告诉编译器"无论您在读取源文本时使用哪种编码,请在将其写入目标文件时将其转换为 UTF-8"。你没有说应该如何解释源文本。这取决于编译器来决定(也许基于你传递给它的标志,也许基于进程的环境,或者可能只是使用硬编码的默认值(

如果源文本中的字符串包含字节0xc5、0xe4、0xf6,并且您告诉它"源文本编码为 ISO-8859",则编译器将识别"字符串由字符 'åäö' 组成。它将看到u8前缀,并将这些字符转换为 UTF-8,将字节序列写入目标文件0xc3、0xa5、0xc3、0xa4、0xc3 0xb6。在这种情况下,您最终会得到一个有效的 UTF-8 编码文本字符串,其中包含字符"åäö"的 UTF-8 表示形式。

但是,如果源文本

中的字符串包含相同的字节,并且您让编译器相信源文本被编码为 UTF-8,则编译器可以执行两件事(取决于实现:

  • 它可能会尝试将字节解析为 UTF-8,在这种情况下,它将识别"这不是有效的 UTF-8 序列",并发出错误。这就是Clang所做的。
  • 或者,它可能会说"好吧,我这里有 3 个字节,我被告知假设它们形成一个有效的 UTF-8 字符串。我会抓住他们,看看会发生什么"。然后,当它应该将字符串写入目标文件时,它会说"好的,我有之前的这 3 个字节,它们被标记为 UTF-8。这里的u8前缀意味着我应该将此字符串编写为 UTF-8。很酷,那么无需进行转换。我只写这 3 个字节,我就完成了"。这就是海湾合作委员会所做的。

两者都有效。C++ 语言没有声明编译器需要检查传递给它的字符串文本的有效性。

但在这两种情况下,请注意u8前缀与您的问题无关。这只是告诉编译器从"字符串读取时的任何编码转换为 UTF-8"。但即使在这种转换之前,字符串就已经乱码了,因为字节对应于 ISO-8859 字符数据,但编译器认为它们是 UTF-8(因为你没有告诉它(。

您看到的问题只是编译器在从源文件中读取字符串文本时不知道要使用哪种编码。

您注意到的另一件事是,没有前缀的"传统"字符串文字将使用编译器喜欢的任何编码进行编码。精确地引入了 u8 前缀(以及相应的 UTF-16 和 UTF-32 前缀(,以允许您指定希望编译器写入输出的编码。纯无前缀文本根本不指定编码,由编译器决定编码。

为了说明这个讨论,这里有一些例子。让我们考虑一下代码:

int main() {
  std::cout << "åäön";
}

1( 使用 g++ -std=c++11 encoding.cpp 编译它将生成一个可执行文件,该可执行文件产生:

% ./a.out | od -txC
0000000 c3 a5 c3 a4 c3 b6 0a

换句话说,每个"字素簇"两个字节(根据 unicode 术语,即在这种情况下,每个字符(,加上最后一个换行符 (0a(。这是因为我的文件是用 utf-8 编码的,cpp 假定输入字符集是 utf-8,而 exec-charset 在 gcc 中默认是 utf-8(参见 https://gcc.gnu.org/onlinedocs/cpp/Character-sets.html(。好。

2(现在,如果我将文件转换为iso-8859-1并使用相同的命令再次编译,我会得到:

% ./a.out | od -txC
0000000 e5 e4 f6 0a

即这三个字符现在使用 ISO-8859-1 进行编码。我不确定这里发生了什么魔术,因为这次似乎 cpp 正确猜测该文件是 iso-8859-1(没有任何提示(,在内部将其转换为 utf-8(根据上面的链接(,但编译器仍然将 iso-8859-1 字符串存储在二进制文件中。我们可以通过查看二进制文件的 .rodata 部分来检查这一点:

% objdump -s -j .rodata a.out
a.out:     file format elf64-x86-64
Contents of section .rodata:
400870 01000200 00e5e4f6 0a00               ..........

(请注意"e5e4f6"字节序列(。
这是完全有意义的,因为使用拉丁语 1 文字的程序员不希望它们在他的程序输出中以 utf-8 字符串的形式出现。

3(现在,如果我保留相同的iso-8859-1编码文件,但使用g++ -std=c++11 -finput-charset=iso-8859-1 encoding.cpp编译,那么我得到一个包含ututs-8数据的二进制文件:

% ./a.out | od -txC
0000000 c3 a5 c3 a4 c3 b6 0a

我觉得这很奇怪:源编码没有改变,我明确告诉 gcc 它是拉丁语-1,结果我得到了 utf-8!请注意,如果我使用 g++ -std=c++11 -finput-charset=iso-8859-1 -fexec-charset=iso-8859-1 encoding.cpp 显式请求 exec-charset ,则可以覆盖:

% ./a.out | od -txC
0000000 e5 e4 f6 0a

我不清楚这两个选项是如何相互作用的......

4(现在让我们将"u8"前缀添加到组合中:

int main() {
  std::cout << u8"åäön";
}

如果文件是utf-8编码的,毫不奇怪地使用默认的字符集(g++ -std=c++11 encoding.cpp(进行编译,则输出也是utf-8。如果我请求编译器在内部使用 iso-8859-1 代替 ( g++ -std=c++11 -fexec-charset=iso-8859-1 encoding.cpp (,输出仍然是 utf-8:

% ./a.out | od -txC
0000000 c3 a5 c3 a4 c3 b6 0a

因此,看起来前缀"u8"阻止了编译器将文字转换为执行字符集。更好的是,如果我将相同的源文件转换为 iso-8859-1,并使用 g++ -std=c++11 -finput-charset=iso-8859-1 -fexec-charset=iso-8859-1 encoding.cpp 进行编译,那么我仍然得到 utf-8 输出:

% ./a.out | od -txC
0000000 c3 a5 c3 a4 c3 b6 0a

因此,似乎"u8"实际上充当了一个"运算符",告诉编译器"将此文字转换为utf-8"。

我通过反复试验发现在 MSVC 上,例如 "ü""u00FC" 没有产生相同的字符串。(当然,ü有代码点 U+00FC。

我的看法是,对于最大可移植的代码,不应依赖于编译器所做的假设或必须告知的编码。

我找到了两种将 UTF-8 放入字符串文字中的可靠方法:

  1. 使用 UTF-8 代码单元,如下所示: "xC3xBC"
  2. u8 前缀与u转义序列结合使用:u8"u00FC"

第一个中,你告诉编译器要做什么,在第二个中,你想要什么。

仅供记录,无前缀"u00FC"u8"ü"都没有在所有平台、编译器和输入编码上为我提供 UTF-8 编码字符串。

至少有两个很好的理由更喜欢u8"su00FCchtig"(süchtig(而不是"sxC3xBCchtig"

  • 您可以在任何合理的字符映射中搜索 U+00FC。
  • u正好需要 4 个十六进制数字,对于非 BMP 字符,U 8 个十六进制数字就可以满足您的需求;另一方面,x消耗尽可能多的十六进制数字,例如 "sxC3xBCchtig"实际上不起作用:它将xBCc视为 1 值,这意味着您必须将字符串拆分为两个文本:"sxC3xBC""chtig"

我仍然无法回答您如何用这个过渡到 C++20,因为u8东西都有自己的类型:char8_t.