我已经阅读并听说 C++11 支持 Unicode。对此有几个问题:
- C++ 标准库对 Unicode 的支持程度如何?
- 做
std::string
它应该做的事吗? - 我该如何使用它?
- 潜在问题在哪里?
C++ 标准库对 unicode 的支持程度如何?
可怕。
快速浏览一下可能提供 Unicode 支持的库设施,我得到了这个列表:
我认为除了第一个之外,所有的都提供了可怕的支持。在快速绕过您的其他问题后,我将更详细地讨论它。
做
std::string
它应该做的事吗?
是的。根据 C++ 标准,这是std::string
和它的兄弟应该做的:
类模板
basic_string
描述的对象可以存储由不同数量的任意类似字符的对象组成的序列,其中序列的第一个元素位于零位置。
嗯,std::string
这样做就好了。这是否提供任何特定于 Unicode 的功能?不。
应该是?可能不是。std::string
作为一个对象序列很好char
。这很有用;唯一的烦恼是它是一个非常低级的文本视图,而标准 C++ 没有提供更高级别的视图。
我该如何使用它?
将其用作char
对象序列;假装它是别的东西注定会以痛苦告终。
潜在问题在哪里?
到处都是?让我们来看看...
字符串库
字符串库为我们提供了basic_string
,它只是标准所谓的“类似字符的对象”的序列。我称它们为代码单元。如果您想要一个高级的文本视图,这不是您想要的。这是适合序列化/反序列化/存储的文本视图。
它还提供了 C 库中的一些工具,可用于弥合狭义世界和 Unicode 世界之间的差距:c16rtomb
/mbrtoc16
和c32rtomb
/ mbrtoc32
。
本地化库
本地化库仍然认为这些“类似字符的对象”之一等于一个“字符”。这当然是愚蠢的,并且除了像 ASCII 这样的 Unicode 的一小部分之外,不可能让很多东西正常工作。
例如,考虑一下<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 ʙᴀɴᴀɴᴀ,如u8""
or u8"\U0001F34C"
?它永远不会起作用,因为这些函数只需要一个代码单元作为输入。
char32_t
如果您仅使用:U'\U0001F34C'
是 UTF-32 中的单个代码单元,这可能适用于适当的语言环境。
但是,这仍然意味着您只能使用 and 进行简单的大小写转换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
。这些标准方面一起提供了以下所有转换。(注意:在下面的列表中,左边的编码总是序列化的 string/streambuf,右边的编码总是反序列化的 string/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>
where sizeof(wchar_t) == 4
;codecvt_utf16<char16_t>
, 和codecvt_utf16<wchar_t>
where sizeof(wchar_t) == 2
;codecvt_utf16<char32_t>
, 和codecvt_utf16<wchar_t>
where sizeof(wchar_t) == 4
;codecvt_utf8_utf16<char16_t>
, codecvt<char16_t, char, mbstate_t>
, 和codecvt_utf8_utf16<wchar_t>
where 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,因为 UTF-8 无法编码 0xD800-0xDFFF 范围内的任何值。
仍然在 UCS-2 前端,没有办法从 UTF-16 字节流读取到具有这些方面的 UTF-16 字符串。如果您有一个 UTF-16 字节序列,则无法将其反序列化为char16_t
. 这是令人惊讶的,因为它或多或少是一种身份转换。然而,更令人惊讶的是,支持从 UTF-16 流反序列化为带有 的 UCS-2 字符串codecvt_utf16<char16_t>
,这实际上是一种有损转换。
不过,UTF-16-as-bytes 支持非常好:它支持从 BOM 中检测字节顺序,或者在代码中显式选择它。它还支持使用和不使用 BOM 生成输出。
缺少一些更有趣的转换可能性。无法将 UTF-16 字节流或字符串反序列化为 UTF-8 字符串,因为从不支持 UTF-8 作为反序列化形式。
在这里,狭义/广义世界与 UTF/UCS 世界完全分开。旧式窄/宽编码与任何 Unicode 编码之间没有转换。
输入/输出库
I/O 库可用于使用上述wstring_convert
和wbuffer_convert
工具读取和写入 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。
☦ 是的,我知道großes Eszett (ẞ),但即使您要在一夜之间将所有德语语言环境更改为将 ß 大写为 ẞ,仍然有很多其他情况会失败。尝试大写 U+FB00 ʟᴀᴛɪɴ sᴍᴀʟʟ ʟɪɢᴀᴛᴜʀᴇ ғғ。没有 ʟᴀᴛɪɴ ᴄᴀᴘɪᴛᴀʟ ʟɪɢᴀᴛᴜʀᴇ ғғ;它只是大写到两个 F。或 U+01F0 ʟᴀᴛɪɴ sᴍᴀʟʟ ʟᴇᴛᴛᴇʀ ᴊ ᴡɪᴛʜ ᴄᴀʀᴏɴ; 没有预先设定的资本;它只是大写字母 J 和组合的 caron。
由于 Unicode NUL (U+0000) 是 UTF-8 中的空字节并且这是空值的唯一方式,因此您可以安全地将 UTF-8 存储在 a std::string
(或 a char[]
or中)char*
字节可以出现在 UTF-8 中。因此,您的 UTF-8 字符串将根据所有 C 和 C++ 字符串函数正确终止,并且您可以使用 C++ iostream(包括std::cout
and std::cerr
,只要您的语言环境是 UTF-8)将它们吊起来。
对于 UTF-8 ,你不能做std::string
的是获取代码点的长度。std::string::size()
将告诉您以字节为单位的字符串长度,当您在 UTF-8 的 ASCII 子集中时,它仅等于代码点的数量。
如果您需要在代码点级别对 UTF-8 字符串进行操作(即不仅仅是存储和打印它们),或者如果您正在处理可能有许多内部空字节的 UTF-16,您需要查看宽字符串类型。
C++11 有几个新的 Unicode 文字字符串类型。
不幸的是,标准库中对非统一编码(如 UTF-8)的支持仍然很糟糕。例如,没有很好的方法来获取 UTF-8 字符串的长度(以代码点为单位)。
但是,有一个非常有用的库叫做tiny-utf8,它基本上是/的替代品。它旨在填补仍然缺失的 utf8-string 容器类的空白。std::string
std::wstring
这可能是“处理” utf8 字符串的最舒适的方式(即,没有 unicode 规范化和类似的东西)。您可以轻松地对codepoints进行操作,而您的字符串仍以 run-length-encoded char
s 进行编码。