C++11 对 Unicode 的支持程度如何?
Posted
技术标签:
【中文标题】C++11 对 Unicode 的支持程度如何?【英文标题】:How well is Unicode supported in C++11? 【发布时间】:2013-06-10 20:00:25 【问题描述】:我已经阅读并听说 C++11 支持 Unicode。几个问题:
C++ 标准库对 Unicode 的支持程度如何?std::string
会做它应该做的事吗?
如何使用它?
潜在问题在哪里?
【问题讨论】:
"std::string 做它应该做的事吗?"你认为它应该怎么做? 我使用 utfcpp.sourceforge.net 来满足我的 utf8 需求。它是一个简单的头文件,为 unicode 字符串提供迭代器。 Unicode 支持的最大潜在问题在于 Unicode 及其在信息技术本身的使用。 Unicode 不适合(也不是设计)它的用途。 Unicode 旨在重现某人在某处编写的每一个可能的字形,在某些时候,每一个可能的和迂腐的细微差别都可能存在,包括 3 或 4 种不同的含义以及 3 或 4 种不同的方式来组成相同的字形。它并不意味着可用于日常语言,也不意味着适用或易于或明确地处理。 是的,它是为用于日常语言而设计的。至少我的。你最有可能也是。事实证明,以一般方式处理人类文本是一项非常困难的任务。甚至不可能明确地定义一个字符是什么。通用字形复制甚至不是 Unicode 章程的一部分。 0x22 和 0x2c 永远不会出现在多字节序列中。 UTF-8 的设计使得每个字节都只是单字节序列,多字节序列的开始,多字节序列的延续中的一个。所以 0x22 总是意味着 U+0022 而 0x2c 总是意味着 U+002C。无论如何,我希望任何这样的库都能正确处理这个问题(即,如果没有,我会责怪库,而不是std::string
;std::string
会做它应该做的一切)
【参考方案1】:
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;标准允许双向转换)。
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。
【讨论】:
我读得越多,就越觉得对这一切一无所知。几个月前我读了大部分这些东西,但仍然觉得我正在重新发现整个事情......为了让我现在有点疼的可怜的大脑保持简单,utf8everywhere 上的所有这些建议仍然存在有效,对吧?如果我“只是”希望我的用户能够打开和写入文件,无论他们的系统设置如何,我都可以询问他们文件名,将其存储在 std::string 中,一切都应该正常工作,即使在 Windows 上也是如此?很抱歉(再次)问这个问题...... @Uflex 你可以真正对 std::string 做的就是把它当作一个二进制 blob。在正确的 Unicode 实现中,内部(因为它隐藏在实现细节中)和外部编码都无关紧要(好吧,你仍然需要有可用的编码器/解码器)。 @Uflex 也许。我不知道遵循您不理解的建议是否是个好主意。 在 C++ 2014/17 中有一个支持 Unicode 的提案。然而,那是 1 年,也许是 4 年之后,现在几乎没有用。 open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3572.html @graham.reeds 哈哈,谢谢,但我知道这一点。检查“致谢”部分;)【参考方案2】:Standard Library 不支持 Unicode(支持的任何合理含义)。
std::string
并不比 std::vector<char>
好:它完全忽略了 Unicode(或任何其他表示/编码),只是将其内容视为 blob 字节。
如果你只需要存储和连接blob,它工作得很好;但是,一旦您希望使用 Unicode 功能(code points 的数量,graphemes 的数量等),那么您就不走运了。
我所知道的唯一综合库是ICU。虽然 C++ 接口是从 Java 接口派生的,但它远非惯用的。
【讨论】:
Boost.Locale 怎么样? @Uflex:来自您链接的页面 为了实现这一目标,Boost.Locale 使用了最先进的 Unicode 和本地化库:ICU - Unicode 的国际组件。 Boost.Locale 支持其他非 ICU 后端,请参见此处:boost.org/doc/libs/1_53_0/libs/locale/doc/html/… @SuperflyJon:是的,但根据同一页面,非 ICU 后端对 Unicode 的支持“非常有限”。【参考方案3】:由于 Unicode NUL (U+0000) 是一个空字节UTF-8,这是在 UTF-8 中出现空字节的唯一方式。因此,您的 UTF-8 字符串将根据所有 C 和 C++ 字符串函数正确终止,并且您可以使用 C++ iostream(包括std::cout
和std::cerr
,只要您的语言环境是 UTF-8 )。
std::string
for UTF-8 不能做的是获取代码点的长度。 std::string::size()
会以 bytes 为单位告诉您字符串长度,当您在 UTF-8 的 ASCII 子集中时,该长度仅等于代码点数。
如果您需要在code point 级别对 UTF-8 字符串进行操作(即不仅仅是存储和打印它们),或者如果您正在处理可能有许多内部空字节的 UTF-16,您需要查看宽字符串类型。
【讨论】:
std::string
可以被放入带有嵌入空值的 iostream 中。
这完全是故意的。它根本不会破坏c_str()
,因为size()
仍然有效。只有损坏的 API(即那些不能像大多数 C 世界那样处理嵌入式空值的 API)会损坏。
Embedded nulls break c_str()
因为c_str()
应该将数据作为以 null 结尾的 C 字符串返回——这是不可能的,因为 C 字符串不能嵌入 null。
不再。 c_str()
现在只返回与 data()
相同的值,即全部返回。具有一定大小的 API 可以使用它。 API 不能,不能。
c_str()
确保结果后跟一个类似 NUL 字符的对象略有不同,而我认为 data()
不会。不,看起来data()
现在也这样做了。 (当然,对于消耗大小而不是从终止符搜索中推断出大小的 API,这不是必需的)【参考方案4】:
C++11 有几个new literal string types 用于 Unicode。
不幸的是,标准库对非统一编码(如 UTF-8)的支持仍然很差。例如,没有很好的方法来获取 UTF-8 字符串的长度(以代码点为单位)。
【讨论】:
如果我们想支持非拉丁语言,我们还需要使用 std::wstring 作为文件名吗?因为新的字符串文字在这里并没有真正的帮助,因为字符串通常来自用户...... @Uflexstd::string
可以hold 没有问题的 UTF-8 字符串,但是例如length
方法返回字符串中的字节数,而不是代码点数。
说实话,获取字符串代码点的长度并没有太多用处。例如,字节长度可用于正确预分配缓冲区。
UTF-8 字符串中的代码点数不是一个非常有趣的数字:可以将ñ
写为 'LATIN SMALL LETTER N WITH TILDE' (U+00F1) (这是一个代码点)或“LATIN SMALL LETTER N”(U+006E)后跟“COMBINING TILDE”(U+0303),这是两个代码点。
所有那些关于“你不需要这个也不需要那个”的cmets,比如“代码点的数量不重要”等等,对我来说听起来有点可疑。一旦您编写了一个应该解析各种 utf8 源代码的解析器,它是否考虑 LATIN SMALL LETTER N'
== (U+006E) followed by 'COMBINING TILDE' (U+0303)
取决于解析器的规范。【参考方案5】:
不过,有一个名为tiny-utf8 的非常有用的库,它基本上是std::string
/std::wstring
的替代品。它旨在填补仍然缺失的 utf8-string 容器类的空白。
这可能是“处理” utf8 字符串的最舒适的方式(即,没有 unicode 规范化和类似的东西)。您可以轻松地对 codepoints 进行操作,而您的字符串仍以运行长度编码的 char
s 编码。
【讨论】:
以上是关于C++11 对 Unicode 的支持程度如何?的主要内容,如果未能解决你的问题,请参考以下文章