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"?" 或 u8"\U0001F34C"?它永远不会起作用,因为这些函数只需要一个代码单元作为输入。
如果您仅使用 char32_t,这可能适用于适当的语言环境:U'\U0001F34C' 是 UTF-32 中的单个代码单元。
但是,这仍然意味着您只能使用 toupper 和 tolower 进行简单的大小写转换,例如,对于某些德语语言环境来说,这还不够好:将“ß”大写字母转换为“SS”☦ 但 @987654348 @ 只能返回一个 character 代码单元。
接下来,wstring_convert/wbuffer_convert 和标准代码转换方面。
wstring_convert 用于将一种给定编码的字符串转换为另一种给定编码的字符串。此转换涉及两种字符串类型,标准称为字节字符串和宽字符串。由于这些术语确实具有误导性,因此我更喜欢分别使用“序列化”和“反序列化”†。
要转换的编码由作为模板类型参数传递给wstring_convert 的 codecvt(代码转换方面)决定。
wbuffer_convert 执行类似的功能,但作为包装 byte 序列化流缓冲区的 wide 反序列化流缓冲区。任何 I/O 都通过底层的 byte 序列化流缓冲区执行,并与 codecvt 参数给出的编码进行转换。写入序列化到该缓冲区,然后从它写入,读取读取到缓冲区,然后从它反序列化。
该标准提供了一些用于这些工具的编解码器类模板:codecvt_utf8、codecvt_utf16、codecvt_utf8_utf16 和一些 codecvt 特化。这些标准方面一起提供了以下所有转换。 (注意:在下面的列表中,左边的编码总是序列化的字符串/streambuf,右边的编码总是反序列化的字符串/streambuf;标准允许双向转换)。
- UTF-8 ↔ UCS-2 与
codecvt_utf8<char16_t> 和codecvt_utf8<wchar_t> 其中sizeof(wchar_t) == 2;
- UTF-8 ↔ UTF-32 与
codecvt_utf8<char32_t>、codecvt<char32_t, char, mbstate_t> 和 codecvt_utf8<wchar_t> 其中sizeof(wchar_t) == 4;
- UTF-16 ↔ UCS-2 与
codecvt_utf16<char16_t> 和codecvt_utf16<wchar_t> 其中sizeof(wchar_t) == 2;
- UTF-16 ↔ UTF-32 与
codecvt_utf16<char32_t> 和codecvt_utf16<wchar_t> 其中sizeof(wchar_t) == 4;
- UTF-8 ↔ UTF-16 与
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,因为 UTF-8 无法编码 0xD800 范围内的任何值-0xDFFF。
仍然在 UCS-2 前端,没有办法从 UTF-16 字节流读取到具有这些方面的 UTF-16 字符串。如果您有一个 UTF-16 字节序列,则无法将其反序列化为 char16_t 字符串。这是令人惊讶的,因为它或多或少是一种身份转换。不过,更令人惊讶的是,支持将 UTF-16 流反序列化为带有 codecvt_utf16<char16_t> 的 UCS-2 字符串,这实际上是一种有损转换。
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++ regexes and 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。