如何在C ++中的UTF-8上正确使用std :: string?
我的平台是Mac和C ++ 11(或更高版本)。 我是C ++初学者,并且从事处理中文和英文的个人项目。 UTF-8是此项目的首选编码。
我读了一些关于Stack Overflow的文章,其中许多文章建议在处理UTF-8时使用std::string
,并避免使用std::wstring
,因为目前没有std::string
用于UTF-8。
但是,他们都没有谈论如何正确处理诸如std::string
、std::wstring
、std::string
或std::regex
之类的函数,因为这些函数在面对UTF-8时通常会返回意外结果。
我应该继续使用std::string
还是切换到std::wstring
? 如果我留在std::string
,那么解决上述问题的最佳实践是什么?
Unicode词汇表
Unicode是一个庞大而复杂的主题。 我不希望在那儿走得太深,但是有必要提供一个简短的词汇表:
- 代码点:代码点是Unicode的基本构建块,代码点只是映射为含义的整数。 整数部分可容纳32位(实际上是24位),其含义可以是字母,变音符号,空格,符号,笑脸,半个旗标,甚至可以是“ 下一部分从右到左读取”。
- 字素簇:字素簇是语义相关的代码点的组,例如unicode中的标志是通过关联两个代码点来表示的; 孤立的这两个中的每一个都没有意义,但是在一个词素簇中关联在一起,它们代表一个标志。 在某些脚本中,字素簇还用于将字母与变音符号配对。
这是Unicode的基础。 代码点和字素簇之间的区别大部分可以被掩盖,因为对于大多数现代语言而言,每个“字符”都映射到单个代码点(对于常用的字母和变音符号组合,有专用的重音形式)。 不过,如果您冒险使用笑脸,旗帜等,那么您可能必须注意区别。
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::string
和"(哈)?"
。
- 如果您担心可移植性,请不要使用
std::string
("(哈)?"
在Windows上仅为16位); 请改用"哈"
(akastd::string
)。 - 内存中表示形式(
std::string
或"(哈)?"
)与磁盘上表示形式(UTF-8,UTF-16或UTF-32)无关,因此请做好在边界转换(读取和写入)的准备。 - 虽然32位
std::string
可以确保一个代码单元代表一个完整的代码点,但仍然不能代表一个完整的字素簇。
如果仅阅读或编写字符串,则std::string
或"(哈)?"
应该没有什么问题。
当您开始切片和切块时,麻烦就开始了,那么您必须注意(1)代码点边界(在UTF-8或UTF-16中)和(2)字素簇边界。 前者可以很容易地自己处理,后者需要使用Unicode感知库。
采摘std::string
或"(哈)?"
?
如果需要考虑性能,则std::string
可能会因其较小的内存占用而表现更好。 尽管大量使用中文可能会改变交易。 一如既往,简介。
如果Grapheme Clusters没问题,那么std::string
具有简化功能的优势:1个代码单位-> 1个代码点意味着您不会意外地拆分代码点,并且"(哈)?"
的所有功能都可以立即使用。
如果您与采用std::string
或"(哈)?"
/"哈"
的软件对接,请坚持使用std::string
,以避免来回转换。 否则会很痛苦。
UTF-8,格式为std::string
。
UTF-8实际上可以在std::string
中很好地工作。
大多数操作都是开箱即用的,因为UTF-8编码是自同步的并且与ASCII向后兼容。
由于代码点的编码方式不同,因此寻找代码点不会偶然匹配另一个代码点的中间部分:
std::string
工程,std::string
用于逐字节匹配字节,- 如果搜索ASCII字符,则
std::string
可以工作。
同样,std::string
应该开箱即用。 由于字符序列("(哈)?"
)只是字节序列("哈"
),因此基本搜索模式应该可以立即使用。
但是,请警惕字符类(例如std::string
),因为取决于正则表达式的风格和实现,它可能匹配Unicode字符,也可能不匹配。
同样,请小心将中继器应用于非ASCII“字符”,std::string
可能仅将最后一个字节视为可选字节; 在这种情况下,请使用括号清楚地描述重复的字节序列:"(哈)?"
。
1查找的关键概念是归一化和归类; 这会影响所有比较操作。 std::string
将始终逐字节比较(并因此进行排序),而不考虑特定于语言或用法的比较规则。 如果需要处理完整的规范化/归类,则需要完整的Unicode库,例如ICU。
find
和regex
都必须使用UTF编码来表示Unicode。 特别是在macOS上,std::string
是UTF-8(8位代码单元),std::wstring
是UTF-32(32位代码单元); 请注意wchar_t
的大小取决于平台。
对于两者,find
跟踪代码单位的数量,而不是代码点或字素簇的数量。 (一个代码点是一个名为Unicode的实体,其中一个或多个组成一个字素簇。字素簇是用户与之交互的可见字符,例如字母或表情符号。)
尽管我对中文的Unicode表示不熟悉,但是很有可能在使用UTF-32时,代码单位的数量通常非常接近字素簇的数量。 但是,显然,这是以使用多达4倍的内存为代价的。
最准确的解决方案是使用Unicode库(例如ICU)来计算您要使用的Unicode属性。
最后,人类语言中不使用组合字符的UTF字符串通常可以很好地与find
/regex
配合使用。 我不确定中文,但是英语就是其中之一。
std::string
和朋友与编码无关。 std::wstring
和std::string
之间的唯一区别是std::wstring
使用wchar_t
作为单个元素,而不是char
。对于大多数编译器,后者是8位。 前者应该足够大以容纳任何unicode字符,但实际上在某些系统上却不是(例如Microsoft的编译器使用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范围内的任何字节。
考虑升级到C ++ 20和std::u8string
,这是我们自2019年以来持有UTF-8最好的东西。 没有标准的库工具可以访问单个代码点或字素簇,但是至少您的类型足够强大,至少可以说它是真正的UTF-8。