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


78

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

我读了一些有关Stack Overflow的文章,其中许多文章建议std::string在处理UTF-8时使用,并避免使用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坚持下去,那么解决上述问题的最佳实践是什么?


13
另请参阅utf8everywhere
Caleth

3
为什么要(以及如何?!)std::wstring与UTF-8一起使用?
乔纳森·威克里

6
std::string::size()仅当您期望ot执行其他操作而不返回字节长度(即代码单位)(而不是字符串中的代码点数)时,才感到惊讶。并str[i]返回字符串中的第i个字节。但是,即使C ++具有char8_t专门用于UTF-8的类型,这仍然是正确的。
乔纳森·威克里

这可能有点题外话,但是为什么要使用C ++?它是Mac上的二等公民,苹果为Objective-C和Swift提供了更好的支持。在它听起来就像你写一个命令行应用程序的基础上,你可能想看看这个。然后,您可以不再担心C ++对Unicode的support脚支持,而继续编写程序。Googleswift unicodeswift regex,一切都为您完成。
保罗·桑德斯

PS:程序实际上是什么的?
保罗·桑德斯

Answers:


113

Unicode词汇表

Unicode是一个庞大而复杂的主题。我不希望在那儿走得太深,但是有必要提供一个简短的词汇表:

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

这是Unicode的基础。因为大多数现代语言中的每个“字符”都映射到单个代码点(对于常用的字母+音素符号,有专用的重音形式),所以代码点和字素簇之间的区别可以被大部分掩盖。不过,如果您冒险使用笑脸,旗帜等,那么您可能必须注意区别。


UTF入门

然后,必须对一系列Unicode Code Points进行编码。通用编码为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可确保一个代码单元代表一个完整的代码点,但它仍不代表一个完整的字素簇。

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

当您开始切片和切块时,麻烦就开始了,那么您必须注意(1)代码点边界(在UTF-8或UTF-16中)和(2)字素簇边界。前者可以很容易地自行处理,后者需要使用Unicode感知库。


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

如果需要考虑性能,则可能std::string由于其较小的内存占用而表现更好。尽管大量使用中文可能会改变交易。一如既往,简介。

如果Grapheme Clusters没问题,那么它std::u32string具有简化操作的优势:1个代码单位-> 1个代码点意味着您不会意外地拆分代码点,并且所有std::basic_string工作功能都可以立即使用。

如果您使用std::stringchar*/来连接软件char const*,则请坚决std::string避免来回转换。否则会很痛苦。


中的UTF-8 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。


感谢您提供的详细信息!我想花点时间弄清楚所有这些!关于原来的问题,此外str.find_first_ofstr.findstd::regex给予好像不是非ASCII投入工作(例如,“哈”或U8“哈”)std::string str(u8"哈哈haha");
stackunderflow

4
@Edityouprofile:str.find("哈")应该可以工作(请参阅ideone.com/s9i1yf),但str.find('哈')不会因为'哈'它是多字节字符。str.find_first_of("哈")将不起作用(仅适用于ASCII模式)。正则表达式对于ASCII模式应该可以正常工作;但是要提防字符类和“重复器”(例如,"哈?"可能仅使最后一个字节成为条件字节)。
Matthieu M.

1
为了可移植性,std::basic_string<char32_t>在* nix和Windows上都能正常工作吗?
昆汀

1
@昆汀:是的。我应该将其添加到替代品列表中!顺便说一句,有一个漂亮的typedef: std::u32string
Matthieu M.

1
str.find("...")str.fin works仅当您只关心逐字节匹配时,否则,您将需要进行适当的规范化和语言环境感知的比较。除此之外,这似乎是一个很好的答案,并说明了为什么我讨厌像Python3这样的语言中存在的Unicode“支持”。
Muzer

10

std::string和朋友都与编码无关。唯一的区别std::wstringstd::stringstd::wstring用途wchar_t作为单独的元素,而不是char。对于大多数编译器,后者为8位。前者应该足够大,可以容纳任何Unicode字符,但实际上在某些系统上却不是(例如Microsoft的编译器使用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范围内的任何字节。


3
“最大扩展字符集的所有成员的不同代码”意味着,如果您的编译器支持Unicode,则单个wchar_t必须能够表示任何有效的Unicode代码点。16位还不够。UTF-16是一种多字节编码;在这里无关紧要。
James Picone

6
这样做的危害在于,std::wstring实际上不应该使用多字节编码。这就是类型的重点。使它成为多字节编码(这是一个不好的编码)只是重复的std::string,但是以一种非常令人讨厌的方式,诱使人们认为他们的代码正确地执行了Unicode。
James Picone

11
@zneak实际上是Unicode的错误,而不是Microsoft的错误。他们告诉微软,字符是16位的,然后微软去把它们变成16位,然后他们说“哎呀,不,他们必须是20.5位”。* nixes没有相同问题的唯一原因是直到做出20.5位决定后,它们才完全不支持Unicode
user253751

4
@zneak UTF-32不同于UTF-16的多字节编码。UTF-16有时需要多个值来表示单个unicode代码点。UTF-32有时需要多个unicode码点来表示单个字素。它们都很棘手,但是在不同级别上却很棘手。
詹姆斯·皮科尼

8
@JamesPicone:“可变宽度编码”可能是比“多字节编码”更合适的术语。
user2357112支持Monica

8

双方std::string 并std::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。我不确定中文,但是英语就是其中之一。


2
感谢您的回答。虽然std::string str(u8"哈哈haha");str.find_first_of(u8"haha");似乎可行,但str.find_first_of(u8"哈ha");始终返回0。而正则表达式似乎也不可行。
stackunderflow

1
@Edityouprofile,这是我的错误:我find_first_of与混淆findfind_first_of不能使用多字节字符。
zneak

11
对于两者,都size跟踪代码点的数量”-错误,它表示代码单位,而不是代码点。巨大差距。“而不是逻辑字符的数量。(逻辑字符是一个或多个代码点。) ”-也被正式称为“字素簇”。
雷米·勒博

2
我不认为该标准必须 std::string在UTF8中,即使我们倾向于在各处都使用UTF8。我想EBCDIC大型机可能会将EBCDIC用于std::string
Basile Starynkevitch,

13
std::string不“使用” UTF-8或EBCDIC的任何编码。std::string只是一个存放类型字节的容器char。您可以在其中放入UTF-8字符串,或者是ASCII字符串,EBCDIC字符串,甚至是二进制数据。这些字节(如果有的话)的编码取决于程序的其余部分以及您对字符串的处理方式,而不是std::string其本身。
乔纳森·威基利

5

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.