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

时间:2018-05-18 03:26:47

标签: c++ string c++11

我的平台是Mac和C ++ 11(或更高版本)。我是一名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,那么处理上述问题的最佳做法是什么?

5 个答案:

答案 0 :(得分:68)

Unicode词汇表

Unicode是一个庞大而复杂的主题。我不想在那里跋涉太深,但是需要一个快速的词汇表:

  1. 代码点:代码点是Unicode的基本构建块,代码点只是映射到含义的整数。整数部分适合32位(嗯,真正的24位),意思可以是字母,变音符号,白色空格,符号,笑脸,半旗...,它甚至可以是“下一部分从右到左阅读“。
  2. Grapheme Clusters :Grapheme Clusters是一组语义相关的Code Points,例如unicode中的标志通过关联两个Code Points来表示;这两者中的每一个都是孤立的,没有任何意义,但在Grapheme集群中它们相互关联,它们代表了一面旗帜。 Grapheme Clusters也用于在某些脚本中将字母与变音符号配对。
  3. 这是Unicode的基础。 Code Point和Grapheme Cluster之间的区别可能大部分被掩盖,因为对于大多数现代语言,每个“字符”都映射到一个代码点(常用字母+变音符组合有专用的重音形式)。不过,如果你冒险使用表情符号,旗帜等......那么你可能需要注意区别。

    UTF Primer

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

    在UTF-X中,X是代码单元的位大小,每个代码点表示为一个或多个代码单元,具体取决于其大小:

    • UTF-8:1到4个代码单位,
    • UTF-16:1或2个代码单元,
    • UTF-32:1代码单元。

    std::stringstd::wstring

    1. 如果您关心可移植性,请不要使用std::wstring(Windows上wchar_t只有16位);请改用std::u32string(又名std::basic_string<char32_t>)。
    2. 内存中表示(std::stringstd::wstring)独立于磁盘上的表示形式(UTF-8,UTF-16或UTF-32),因此请准备好进行转换在边界(阅读和写作)。
    3. 虽然32位wchar_t确保代码单元代表完整的代码点,但它仍然不代表完整的字形集群。
    4. 如果您只是阅读或撰写字符串,那么std::stringstd::wstring应该没有什么问题。

      当您开始切片和切块时,麻烦就开始了,那么您必须注意(1)代码点边界(UTF-8或UTF-16)和(2)Grapheme Clusters边界。前者可以自己轻松处理,后者需要使用Unicode感知库。

      挑选std::stringstd::u32string

      如果性能受到关注,std::string可能因其较小的内存占用而表现更好;虽然大量使用中国人可能会改变这笔交易。一如既往,简介。

      如果Grapheme Clusters不是问题,那么std::u32string具有简化事物的优点:1代码单元 - &gt; 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字符匹配,也可能不匹配。

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

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

答案 1 :(得分:8)

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

如果要通过Unicode代码点或组合的unicode字形(或其他东西)索引UTF-8字符串,请计算Unicode代码点或其他某些unicode对象中的UTF-8字符串的长度,或者通过Unicode代码点查找,你将需要使用标准库以外的东西。 ICU是该领域的图书馆之一;可能还有其他人。

可能值得注意的是,如果您正在搜索ASCII字符,则可以将UTF-8字节流视为逐字节处理。每个ASCII字符在UTF-8中编码与在ASCII中编码相同,并且UTF-8中的每个多字节单元都保证不包含ASCII范围内的任何字节。

答案 2 :(得分:8)

std::stringstd::wstring都必须使用UTF编码来表示Unicode。特别是在macOS上,std::string是UTF-8(8位代码单元),std::wstring是UTF-32(32位代码单元);请注意,wchar_t的大小取决于平台。

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

虽然我不熟悉中文的Unicode表示,但是当你使用UTF-32时,代码单元的数量通常非常接近字形集群的数量。然而,显然,这需要使用多达4倍的内存。

最准确的解决方案是使用Unicode库(如ICU)来计算您所追求的Unicode属性。

最后,人类语言中不使用组合字符的UTF字符串通常与find / regex非常相似。我不确定中文,但英文就是其中之一。

答案 3 :(得分:0)

考虑到升级到C ++ 20和std::u8string,这是我们自2019年以来拥有UTF-8最好的东西。没有标准的库工具可以访问单个代码点或字素簇,但至少您的类型足够强大,至少可以说它是真正的UTF-8。

答案 4 :(得分:0)

<块引用>

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

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

大多数标准字符串操作使用 UTF-8,但使用 code units。如果您想要更高级别的 API,则必须使用其他内容,例如向 Boost 提议的 the text library