我已经听说C ++ 11支持Unicode。关于此的几个问题:
- C ++标准库对Unicode的支持程度如何?
- 请问
std::string
该怎么办? - 如何使用?
- 潜在的问题在哪里?
我已经听说C ++ 11支持Unicode。关于此的几个问题:
std::string
该怎么办?Answers:
C ++标准库对Unicode的支持程度如何?
可怕。
快速浏览可能提供Unicode支持的库功能后,我得到了以下列表:
我认为除了第一个以外,其他所有人都提供了可怕的支持。在绕过您的其他问题后,我将更详细地介绍它。
请问
std::string
该怎么办?
是。根据C ++标准,这是std::string
它及其同级应该做的:
类模板
basic_string
描述了一些对象,这些对象可以存储由不同数量的任意char型对象组成的序列,该序列的第一个元素位于位置0。
好吧,std::string
那还好吗。这是否提供任何特定于Unicode的功能?没有。
应该是?可能不是。std::string
可以作为一系列char
对象。这很有用;唯一的烦恼是它是非常低级的文本视图,而标准的C ++没有提供更高级别的视图。
如何使用?
将其用作char
对象序列;假装这是其他的事情,注定要以痛苦结束。
潜在的问题在哪里?
到处都是?让我们来看看...
字符串库
字符串库为我们提供了信息basic_string
,这仅仅是标准称为“类似字符的对象”的序列。我称它们为代码单元。如果您想要高级的文本视图,这不是您想要的。这是适合于序列化/反序列化/存储的文本视图。
它还提供了C库中的一些工具,可以用来弥合狭窄世界和Unicode世界之间的鸿沟:c16rtomb
/ mbrtoc16
和c32rtomb
/ mbrtoc32
。
本地化库
本地化库仍然认为那些“字符状对象”之一等于一个“字符”。这当然是愚蠢的,并且使得除了某些小的Unicode子集(例如ASCII)之外,无法使许多事情正常工作。
例如,考虑一下<locale>
标头中标准所称的“便捷接口” :
template <class charT> bool isspace (charT c, const locale& loc);
template <class charT> bool isprint (charT c, const locale& loc);
template <class charT> bool iscntrl (charT c, const locale& loc);
// ...
template <class charT> charT toupper(charT c, const locale& loc);
template <class charT> charT tolower(charT c, const locale& loc);
// ...
您如何期望这些功能中的任何一个正确地归类为U + 1F34Cʙᴀɴᴀɴᴀ,如in u8"🍌"
或u8"\U0001F34C"
?这是不可能的,因为这些功能仅采用一个代码单元作为输入。
如果char32_t
仅使用以下语言,则可以在适当的语言环境下工作:U'\U0001F34C'
是UTF-32中的单个代码单元。
但是,这仍然意味着您只能使用toupper
和进行简单的大小写转换tolower
,例如,对于某些德语语言环境来说,这是不够的:“ß”大写字母转换为“ SS”☦,但toupper
只能返回一个字符代码单位。
接下来,wstring_convert
/ wbuffer_convert
和标准代码转换方面。
wstring_convert
用于在一种给定编码的字符串之间转换为另一种给定编码的字符串。此转换涉及两种字符串类型,标准将其称为字节字符串和宽字符串。由于这些术语确实具有误导性,因此我宁愿分别使用“序列化”和“反序列化”†。
在之间进行转换的编码由作为模板类型参数传递给的codecvt(代码转换构面)决定wstring_convert
。
wbuffer_convert
执行类似的功能,但是用作包装字节序列化流缓冲区的宽反序列化流缓冲区。任何I / O都通过底层字节序列化流缓冲区执行,并与codecvt参数给出的编码进行来回转换。写入会序列化到该缓冲区中,然后从中进行写入,读取会读入缓冲区,然后从中反序列化。
本标准规定了一些的codecvt类模板与这些设施的使用:codecvt_utf8
,codecvt_utf16
,codecvt_utf8_utf16
,和一些codecvt
专业。这些标准方面共同提供了以下所有转换。(注意:在下面的列表中,左侧的编码始终是序列化的字符串/ streambuf,而右侧的编码始终是反序列化的字符串/ streambuf;该标准允许双向转换)。
codecvt_utf8<char16_t>
,和codecvt_utf8<wchar_t>
其中sizeof(wchar_t) == 2
;codecvt_utf8<char32_t>
,codecvt<char32_t, char, mbstate_t>
以及codecvt_utf8<wchar_t>
其中sizeof(wchar_t) == 4
;codecvt_utf16<char16_t>
,和codecvt_utf16<wchar_t>
其中sizeof(wchar_t) == 2
;codecvt_utf16<char32_t>
,和codecvt_utf16<wchar_t>
其中sizeof(wchar_t) == 4
;codecvt_utf8_utf16<char16_t>
,codecvt<char16_t, char, mbstate_t>
以及codecvt_utf8_utf16<wchar_t>
其中sizeof(wchar_t) == 2
;codecvt<wchar_t, char_t, mbstate_t>
codecvt<char, char, mbstate_t>
。其中一些有用,但是这里有很多尴尬的东西。
首先-神圣的代理!命名方案很混乱。
然后,有很多UCS-2支持。UCS-2是Unicode 1.0的一种编码,由于仅支持基本的多语言平面,因此于1996年被取代。我不知道为什么委员会认为需要关注20年前被取代的编码。并不是说对更多编码的支持不好或什么,但是UCS-2在这里经常出现。
我要说的char16_t
是,这显然是为了存储UTF-16代码单元。但是,这是该标准另有考虑的一部分。codecvt_utf8<char16_t>
与UTF-16无关。例如,wstring_convert<codecvt_utf8<char16_t>>().to_bytes(u"\U0001F34C")
将编译良好,但会无条件失败:输入将被视为UCS-2字符串u"\xD83C\xDF4C"
,由于UTF-8无法编码0xD800-0xDFFF范围内的任何值,因此无法将其转换为UTF-8。
仍然处于UCS-2前端,无法通过这些方面将UTF-16字节流读取为UTF-16字符串。如果您使用UTF-16字节序列,则无法将其反序列化为字符串char16_t
。这是令人惊讶的,因为它或多或少是身份转换。但是,更令人惊讶的是,存在这样的事实,即支持使用UTF-16流从UTF-16流反序列化为UCS-2字符串codecvt_utf16<char16_t>
,这实际上是有损转换。
但是,UTF-16-as-bytes支持非常好:它支持从BOM表中检测字节序,或在代码中明确选择字节序。它还支持生成带有或不带有BOM的输出。
缺少一些更有趣的转换可能性。由于从不支持将UTF-8作为反序列化形式,因此无法从UTF-16字节流或字符串反序列化为UTF-8字符串。
在这里,狭窄/宽阔的世界与UTF / UCS完全分开。旧式的窄/宽编码与任何Unicode编码之间都没有转换。
输入/输出库
使用上述wstring_convert
和wbuffer_convert
功能,可以使用I / O库以Unicode编码读取和写入文本。我认为标准库的这一部分不需要其他很多支持。
正则表达式库
之前,我已经在Stack Overflow上阐述了C ++正则表达式和Unicode的问题。我在这里不会重复所有这些要点,而只是声明C ++正则表达式不具有1级Unicode支持,这是使它们在不依赖于在任何地方都使用UTF-32的情况下可用的最低要求。
而已?
对,就是那样。那就是现有的功能。Unicode功能很多,像规范化或文本分段算法一样,无处可寻。
U + 1F4A9。有什么方法可以在C ++中获得更好的Unicode支持吗?
通常的嫌疑犯:ICU和Boost.Locale。
†毫不奇怪,字节字符串是一个字节字符串,即char
对象。但是,与通常是对象数组的宽字符串文字不同wchar_t
,本文中的“宽字符串”不一定是wchar_t
对象字符串。实际上,该标准从未明确定义“宽字符串”的含义,因此我们只能从用法中猜测含义。由于标准术语草率且令人困惑,因此我以清楚的名义使用了自己的术语。
像UTF-16这样的编码可以存储为的序列char16_t
,这些序列没有字节序;也可以将它们存储为具有字节序的字节序列(每个连续的字节对char16_t
根据字节序可以表示不同的值)。该标准支持这两种形式。序列的序列char16_t
对于程序中的内部操作更有用。字节序列是与外部世界交换此类字符串的方式。因此,我将代替“字节”和“宽”使用的术语是“序列化”和“反序列化”。
‡如果您要说“但是Windows!”。握住你的🐎🐎。自Windows 2000以来的所有Windows版本都使用UTF-16。
☦是的,我知道Eszett(ẞ)的问题,但是即使您一夜之间将所有德国语言环境都更改为将ß变为upper,也有很多其他情况会失败。尝试使用大写U + FB00ʟᴀᴛɪʟᴀᴛsᴍᴀʟʟᴍᴀʟʟɪɢᴀᴛᴜʀᴇғғ。没有ʟᴀᴛɪɴᴄᴀᴘɪʟʟɪɢᴀᴛᴜʀᴇғғ;它只是大写到两个F。或U + 01F0ʟᴀᴛɪɴsᴍᴀʟʟᴡᴡɪᴛʜᴄᴀʀᴏɴ; 没有预先设定的资本;它只是大写的大写字母J和组合的caron。
标准库不支持Unicode (出于所支持的任何合理含义)。
std::string
没有更好的比std::vector<char>
:它是完全无视的Unicode(或任何其它表示/编码)和简单地对待它作为内容的blob的字节。
如果只需要存储和分类blob,则效果很好。但是一旦您希望使用Unicode功能(代码点数量,字素数量等),您就会很不走运。
我所知道的唯一全面的库是ICU。尽管C ++接口是从Java派生的,所以它远不是惯用的。
您可以UTF-8在安全存储std::string
(或者在一个char[]
或char*
,对于这个问题),由于是一个Unicode NUL(U + 0000)是UTF-8空字节,这是唯一的办法空字节可以出现在UTF-8中。因此,您的UTF-8字符串将根据所有C和C ++字符串函数正确终止,并且您可以使用C ++ iostream(包括std::cout
和std::cerr
,只要您的语言环境为UTF-8)将它们悬挂起来。
std::string
对于UTF-8,您不能做的就是获得代码点的长度。std::string::size()
会告诉您以字节为单位的字符串长度,该长度仅等于您位于UTF-8的ASCII子集中时的代码点数。
如果您需要在代码点级别上对UTF-8字符串进行操作(即,不仅存储和打印它们),或者正在处理UTF-16(它可能有许多内部空字节),则需要研究一下宽字符串类型。
std::string
可以扔进带有嵌入式null的iostream中,就好了。
c_str()
,因为它size()
仍然有效。只有损坏的API(即,像大多数C语言世界一样,不能处理嵌入式null的API)才会损坏。
c_str()
是因为c_str()
应该以空值终止的C字符串形式返回数据-这是不可能的,因为C字符串不能具有嵌入的空值。
c_str()
现在只需返回与相同的值data()
,即全部返回。占用大小的API可以使用它。没有的API不能。
c_str()
可以确保结果后跟一个类似NUL char的对象,但我认为data()
不会。不,看起来data()
现在也一样。(当然,对于使用大小而不是通过终止符搜索来推断大小的API而言,这不是必需的)
C ++ 11 为Unicode 提供了一些新的文字字符串类型。
不幸的是,标准库对非统一编码(如UTF-8)的支持仍然很差。例如,没有很好的方法来获取UTF-8字符串的长度(以代码点为单位)。
std::string
可以毫无问题地容纳 UTF-8字符串,但是例如该length
方法返回字符串中的字节数,而不是代码点数。
ñ
为“带小标题的拉丁文小写字母N”(U + 00F1)(这是一个代码点)或“拉丁文小写字母N”( U + 006E)后跟“ COMBINING TILDE”(U + 0303),这是两个代码点。
LATIN SMALL LETTER N'
== 取决于解析器的规范(U+006E) followed by 'COMBINING TILDE' (U+0303)
。