如何在 C + + 中对 UTF-8正确使用 std: : string?

我的平台是 Mac。我是一个 C + + 初学者,正在从事一个处理中文和英文的个人项目。UTF-8是此项目的首选编码。

我读了一些关于 Stack Overflow 的文章,其中许多文章建议在处理 UTF-8时使用 std::string,避免使用 wchar_t,因为目前没有针对 UTF-8的 char8_t

然而,它们都没有谈到如何正确处理像 str[i]std::string::size()std::string::find_first_of()std::regex这样的函数,因为这些函数在面对 UTF-8时通常会返回意想不到的结果。

我应该继续使用 std::string还是切换到 std::wstring?如果我继续使用 std::string,那么处理上述问题的最佳实践是什么?

102307 次浏览

std::string和朋友是不可编码的。std::wstringstd::string之间的唯一区别是,std::wstring使用 wchar_t作为单独的元素,而不是 char。对于大多数编译器来说,后者是8位的。前者应该足够大,可以容纳任何 Unicode字符,但在一些系统上实际上并非如此(例如,微软的编译器使用16位类型)。您不能将 UTF-8存储在 std::wstring中; 这不是设计它的目的。它被设计成相当于 UTF-32-一个字符串,其中每个元素都是一个 Unicode 代码点。

如果你想通过 Unicode 编码点或组合的 Unicode 标志符号(或其他东西)来索引 UTF-8字符串,在 Unicode 编码点或其他 Unicode 对象中计算 UTF-8字符串的长度,或者通过 Unicode 编码点查找,你需要使用标准库以外的东西。重症监护室是该领域的库之一,可能还有其他库。

值得注意的是,如果搜索 ASCII 字符,大多数情况下可以将 UTF-8字节串视为一个字节一个字节地对待。每个 ASCII 字符在 UTF-8中的编码与在 ASCII 中的编码相同,并且保证 UTF-8中的每个多字节单元不包括 ASCII 范围内的任何字节。

Both std::string and std::wstring must use UTF encoding to represent Unicode. On macOS specifically, std::string is UTF-8 (8-bit code units), and std::wstring is UTF-32 (32-bit code units); note that the size of wchar_t is platform-dependent.

对于这两种情况,size都跟踪代码单元的数量,而不是代码点的数量或字形集群的数量。(代码点是一个名为 Unicode 的实体,其中一个或多个形成一个字符集群。字母群是用户与之交互的可见字符,如字母或表情符号。)

虽然我不熟悉中文的 Unicode 表示法,但是当您使用 UTF-32时,很有可能代码单元的数量通常与字母簇的数量非常接近。然而,显然,这样做的代价是使用多达4倍的内存。

The most accurate solution would be to use a Unicode library, such as ICU, to calculate the Unicode properties that you are after.

最后,人类语言中不使用组合字符的 UTF 字符串通常可以很好地处理 find/regex。我对汉语不是很确定,但英语是其中之一。

Unicode 词汇表

Unicode 是一个庞大而复杂的主题。我不希望在这个问题上涉及太多,但有必要简要说明一下:

  1. 代码点 : 代码点是 Unicode 的基本构建块,代码点只是映射到 意义的一个整数。整数部分分为32位(实际上是24位) ,其含义可以是一个字母、一个变音符、一个空格、一个符号、一个笑脸、半面旗子,... ... 甚至可以是“下一部分从右向左读”。
  2. 字符集群 : 字符集群是语义相关的代码点组,例如 Unicode 中的一个标志通过关联两个代码点来表示; 这两个单独的代码点没有任何意义,但是在一个字符集群中,它们关联在一起表示一个标志。在某些脚本中,字母簇还用于将字母与变音符配对。

这是 Unicode 的基础。代码点(Code Point)和字母集群(Grapheme Cluster)之间的区别可以忽略不计,因为对于大多数现代语言来说,每个“字符”都被映射到一个代码点(Code Point)(对于常用的字母 + 变音组合,有专门的重音形式)。不过,如果你冒险使用表情符号、旗帜等... ..。.那么你可能要注意这两者之间的区别。


UTF Primer

然后,必须对一系列 Unicode 编码点进行编码; 常见的编码是 UTF-8、 UTF-16和 UTF-32,后两种编码同时以 Little-Endian 和 Big-Endian 形式存在,共有5种常见编码。

在 UTF-X 中,X 是以位为单位的 密码小组,每个代码点表示为一个或几个代码单位,这取决于它的大小:

  • UTF-8: 1 to 4 Code Units,
  • UTF-16:1或2代码单元,
  • 编码单元。

std::stringstd::wstring

  1. 如果您关心可移植性,就不要使用 std::wstring(wchar_t在 Windows 上只有16位) ; 而是使用 std::u32string(又名 std::basic_string<char32_t>)。
  2. 内存中的表示(std::stringstd::wstring)独立于磁盘上的表示(UTF-8、 UTF-16或 UTF-32) ,因此要做好必须在边界(读和写)进行转换的准备。
  3. 虽然32位 wchar_t确保代码单元代表一个完整的代码点,但它仍然不能代表一个完整的字母集群。

如果您只是读取或编写字符串,那么对于 std::stringstd::wstring应该没有什么问题。

当你开始切片和切片时,麻烦就开始了,然后你必须注意(1)代码点边界(UTF-8或 UTF-16)和(2)图形集群边界。前者可以很容易地自己处理,后者需要使用支持 Unicode 的库。


选择 std::string还是 std::u32string

如果性能是一个问题,那么 std::string的性能可能会更好,因为它的内存占用更小; 尽管大量使用中文可能会改变交易。一如既往,侧写。

如果图形集群不是问题,那么 std::u32string有简化事情的优势: 1代码单元-> 1代码点意味着你不能意外地分割代码点,而且 std::basic_string的所有功能都可以开箱即用。

如果你接口的软件采取 std::stringchar*/char const*,然后坚持 std::string,以避免来回转换。否则会很痛苦。


std::string中的 UTF-8。

UTF-8实际上在 std::string中工作得很好。

由于 UTF-8编码是自同步的,并且与 ASCII 向后兼容,因此大多数操作都可以立即执行。

由于代码点的编码方式,寻找一个代码点不能意外地匹配中间的另一个代码点:

  • str.find('\n')运行正常,
  • str.find("...")工作 一个字节一个字节地匹配1,
  • str.find_first_of("\r\n")工作 如果搜索 ASCII 字符

类似地,regex应该基本上是开箱即用的。由于字符序列("haha")只是一个字节序列("哈") ,所以基本的搜索模式应该是开箱即用的。

但是,要小心字符类(如 [:alphanum:]) ,因为它可能与 Unicode 字符匹配,也可能与 Unicode 字符不匹配,这取决于正则表达式的风格和实现。

同样,对于非 ASCII“字符”应用中继器时也要小心,"哈?"可能只考虑最后一个字节是可选的,在这种情况下使用括号清楚地描述字节的重复序列:。

查找的关键概念是规范化和排序,这会影响所有的比较操作。std::string总是一个字节一个字节地比较(从而排序) ,而不考虑特定于某种语言或用法的比较规则。如果需要处理完整的规范化/排序,则需要一个完整的 Unicode 库,例如 ICU。

考虑升级到 C + + 20和 std::u8string,这是2019年保存 UTF-8的最佳选择。虽然没有标准的库工具来访问单独的代码点或字形集群,但至少您的类型足够强大,至少可以说它是真正的 UTF-8。

我应该继续使用 std::string还是切换到 std::wstring

我建议使用 std::string,因为 wchar_t是不可移植的,而且 C + + 20 char8_t在标准中的支持很差,根本不受任何系统 API 的支持(而且可能永远不会受到兼容性原因的影响)。在包括 macOS 在内的大多数平台上,使用普通 char字符串的字符串已经是 UTF-8。

大多数标准字符串运算都可以使用 UTF-8,但在 代码单位上运行。如果您想要一个更高级的 API,那么您必须使用其他一些东西,比如提议 Boost 的 文本库