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

sta*_*low 57 c++ string c++11

我的平台是Mac和C++ 11(或更高版本).我是一名C++初学者,正在处理一个处理中文和英文的个人项目.UTF-8是此项目的首选编码.

我在Stack Overflow上阅读了一些帖子,其中许多人建议std::string在处理UTF-8时使用,并避免wchar_t因为char8_tUTF-8现在没有.

然而,他们没有谈论如何正确地与像函数处理str[i],std::string::size(),std::string::find_first_of()或者std::regex因为这些功能通常面临UTF-8时,返回意外的结果.

我应该继续std::string或切换到std::wstring?如果我应该std::string坚持下去,那么处理上述问题的最佳做法是什么?

Mat*_* M. 82

Unicode词汇表

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

  1. 代码点:代码点是Unicode的基本构建块,代码点只是映射到含义的整数.整数部分适合32位(嗯,真正的24位),意思可以是字母,变音符号,白色空格,符号,笑脸,半旗,......它甚至可以是"下一部分从右到左阅读".
  2. 字形集群:字形集群是与语义相关的代码点组,例如,unicode中的标志通过关联两个代码点来表示; 这两者中的每一个都是孤立的,没有任何意义,但在Grapheme集群中它们相互关联,它们代表了一面旗帜.Grapheme Clusters也用于在一些脚本中将字母与变音符号配对.

这是Unicode的基础.Code Point和Grapheme Cluster之间的区别可能大部分被掩盖,因为对于大多数现代语言,每个"字符"都映射到一个代码点(常用字母+变音符组合有专用的重音形式).不过,如果你冒险使用表情符号,旗帜等......那么你可能不得不注意区别.


UTF入门

然后,必须编码一系列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如果你关心可移植性,请不要使用(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确保代码单元代表完整的代码点,但它仍然不代表完整的Grapheme集群.

如果你只阅读和撰写串,你应该没有与少的问题std::stringstd::wstring.

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


采摘std::string还是std::u32string

如果性能是一个问题,std::string由于其较小的内存占用,它可能会表现更好; 虽然大量使用中国人可能会改变这笔交易.一如既往,简介.

如果Grapheme Clusters不是问题,则std::u32string具有简化事物的优点:1代码单元 - > 1代码点意味着您不会意外地分割代码点,并且所有功能都是std::basic_string开箱即用的.

如果您与软件接口std::stringchar*/ 接口char const*,则坚持std::string避免来回转换.否则这将是一种痛苦.


UTF-8 in std::string.

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.

  • @Edityouprofile:`str.find("ha")` 应该可以工作(参见 https://ideone.com/s9i1yf),但是 `str.find('ha')` 不会,因为 `'ha'` 是一个多- 字节字符。`str.find_first_of("ha")` 将不起作用(仅适用于 ASCII 模式)。正则表达式应该适用于 ASCII 模式;但是要注意字符类和“中继器”(例如,`"ha?"` 可能只使最后一个字节有条件)。 (4认同)
  • @昆汀:是的。我应该将它添加到替代品列表中!顺便说一句,有一个漂亮的 typedef:`std::u32string`。 (2认同)
  • 仅当您只关心逐字节匹配时,`str.find("...")str.fin 才有效 - 否则您将需要适当的规范化和区域设置感知比较。除此之外,这似乎是一个非常好的答案,并且说明了为什么我有点讨厌 Python3 等语言中存在的 Unicode“支持”。 (2认同)

zne*_*eak 9

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

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

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

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

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

  • "*对于两者,'size`跟踪代码点的数量*" - 错误,它代表**代码单位**,而不是**代码点**.很大的区别."*而不是逻辑字符的数量.(逻辑字符是一个或多个代码点.)*" -​​ 也更正式地称为Grapheme集群. (11认同)
  • `std :: string`不"使用"任何编码,既不是UTF-8也不是EBCDIC.`std :: string`只是`char`类型字节的容器.您可以在其中放置U​​TF-8字符串,或ASCII字符串,或EBCDIC字符串,甚至二进制数据.这些字节的编码(如果有的话)由你的程序的其余部分以及你对字符串做什么决定,而不是由`std :: string`本身决定. (9认同)
  • 谢谢你的回答.当`std :: string str(u8"哈哈哈哈"); str.find_first_of(u8"哈哈");`似乎工作,`str.find_first_of(u8"哈哈");`总是返回0.而正则表达式似乎不工作. (2认同)
  • 我不认为标准 * 要求 * `std::string` 使用 UTF8,即使我们倾向于 [UTF8 无处不在](http://utf8everywhere.org/)。我猜 EBCDIC 大型机可能会将 EBCDIC 用于 `std::string` (2认同)

Jam*_*one 8

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

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

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

  • @zneak它实际上是Unicode的错,而不是微软的错.他们告诉微软人物是16位,然后微软去了16位,然后他们说"哎呀,不,他们必须是20.5位".唯一的原因\*nixes没有相同的问题是因为他们*根本不支持Unicode*直到做出20.5位的决定之后. (10认同)
  • @JamesPicone:"可变宽度编码"可能是比"多字节编码"更合适的术语. (7认同)
  • 危害是`std :: wstring`实际上不应该是多字节编码; 这就是类型的重点.使它成为一个多字节编码(并且是一个糟糕的编码)只是复制`std :: string`,但是以一种非常讨厌的方式欺骗​​人们认为他们的代码正确地执行了Unicode. (6认同)
  • "最大扩展字符集的所有成员的不同代码"表示如果编译器支持Unicode,则单个wchar_t必须能够表示任何有效的Unicode代码点.16位是不够的.UTF-16是一种多字节编码; 它与此无关. (3认同)
  • @zneak UTF-32不是UTF-16的多字节编码方式.UTF-16有时需要多个值来表示单个unicode代码点.UTF-32有时需要多个unicode代码点来表示单个字素.它们都很棘手,但它们在不同层次上都很棘手. (3认同)
  • 我不相信这是暗示.UTF-16是编码Unicode的完美方式. (2认同)
  • @JamesPicone,没有解释UTF-32获得的免费通行证.每个UTF编码(包括UTF-32)都是多字节编码. (2认同)
  • “代码点大小”和“字素大小”是不同的问题,您希望前者多于后者。“过滤掉这个代码点”和“过滤掉这个字素”是不同的问题,你想要前者多于后者。等等。 (2认同)
  • 实际上["在接下来的几天里,Pike和Thompson实施了它并更新了Plan 9以便在整个过程中使用它,然后将它们的成功传达回X/Open,后者将其作为FSS-UTF的规范接受"](https:/ /en.wikipedia.org/wiki/UTF-8#History)所以计划9甚至在正式发布UTF-8之前就已经获得了UTF-8支持 (2认同)

小智 5

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

  • 绝对避免使用 u8string,因为它在标准中的支持很差。你甚至无法输出它。 (8认同)