C++11对Unicode的支持如何?

191

我已经阅读和听说C++11支持Unicode。几个相关问题:

  • C++标准库对Unicode的支持如何?
  • std::string是否能够正常工作?
  • 我该如何使用它?
  • 有哪些可能存在的问题?

22
"std::string是否做了它应该做的事情?" 你认为它应该做什么? - R. Martinho Fernandes
2
我使用http://utfcpp.sourceforge.net/来满足我的utf8需求。它是一个简单的头文件,为Unicode字符串提供迭代器。 - fscan
4
Unicode支持面临的最大潜在问题在于Unicode本身以及其在信息技术中的使用。Unicode不适用(也没有设计)于它所用于的用途。Unicode的设计目的是为了复制每个可能由某人某时某地写过的字形,包括所有不太可能和学究式的细微差别,其中包括3或4个不同的含义和3或4种不同的组合方式。它并非用于日常语言的实用工具,并且并非旨在易于或无歧义地处理。 - Damon
14
是的,它被设计为可以用于日常语言。至少是我的。而你的也很可能如此。只是处理人类文本在一般情况下是一项非常困难的任务。甚至无法明确地定义一个字符是什么。一般字形再现甚至不是 Unicode 宪章的真正部分。 - Jean-Denis Muys
3
0x22和0x2c永远不会同时出现在多字节序列中。UTF-8的设计是每个字节只属于{单字节序列、多字节序列的起始、多字节序列的继续}之一。因此,0x22始终表示U+0022,而0x2c始终表示U+002C。无论如何,我期望任何这样的库都能正确处理此类情况(即如果不能,请责怪库,而不是“std :: string”;“std :: string”会执行其应有的功能)。 - R. Martinho Fernandes
显示剩余4条评论
5个回答

280
C++标准库对Unicode的支持如何?糟糕透了。快速扫描可能提供Unicode支持的库设施,我得到了以下列表:字符串库、本地化库、输入/输出库和正则表达式库。我认为除了第一个之外,其他都提供了可怕的支持。在快速浏览完您的其他问题后,我将更详细地回答它。
std::string是否做了应该做的事情?是的。根据C++标准,这就是std::string及其兄弟应该做的事情:
“basic_string类模板描述了可以存储由任意char-like对象组成的序列的对象,其中序列的第一个元素位于位置0。”好吧,std::string做得很好。这是否提供了任何特定于Unicode的功能?没有。

它需要吗?可能不需要。 std :: string 作为一组 char 对象是好的。这很有用;唯一的烦恼是它是文本的一个非常低级别的视图,标准C ++不提供更高级别的视图。

我应该如何使用它?

将其用作 char 对象的序列;假装它是其他东西注定会带来痛苦。

潜在的问题在哪里?

到处都是?让我们看看...

字符串库

字符串库为我们提供了basic_string ,它只是标准称之为“char-like对象”的序列。我称它们为代码单元。如果您想获得文本的高级别视图,则不是您要寻找的内容。这是适用于序列化/反序列化/存储的文本视图。

它还提供了从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 ʙᴀɴᴀɴᴀ,例如在u8""或u8"\U0001F34C"中?它们永远无法正常工作,因为这些函数只接受一个代码单元作为输入。
如果仅使用char32_t,则适当的区域设置可能会起作用:U'\U0001F34C'是UTF-32中的一个代码单元。
然而,这仍意味着您只能使用toupper和tolower来进行简单的大小写转换,例如对于某些德语区域设置不够好:"ß"大写为"SS",但toupper只能返回一个字符代码单元。
接下来是wstring_convert/wbuffer_convert和标准代码转换facet。

wstring_convert用于在给定的编码之间转换字符串。此转换涉及两种字符串类型,标准称为字节串和宽串。由于这些术语实际上很容易引起误解,我更喜欢分别使用“序列化”和“反序列化”。

要转换的编码由传递给wstring_convert作为模板类型参数的codecvt(代码转换facet)决定。

wbuffer_convert执行类似的功能,但作为包装字节序列化流缓冲区的反序列化流缓冲区。通过底层字节序列化流缓冲区进行任何I/O,并通过codecvt参数进行编码转换。写入序列化到该缓冲区,然后从中写入,读取读入缓冲区,然后从其中反序列化。

本标准提供了一些用于这些设施的codecvt类模板:codecvt_utf8、codecvt_utf16、codecvt_utf8_utf16以及一些codecvt特化。这些标准facet共同提供了以下所有转换。(注意:在以下列表中,左侧编码始终是序列化字符串/流缓冲区,右侧编码始终是反序列化字符串/流缓冲区;标准允许双向转换)。
  • 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 字节流中读取并反序列化为 char16_t 字符串。这很令人惊讶,因为这几乎是一种身份转换。更令人惊讶的是,codecvt_utf16<char16_t> 支持从 UTF-16 流反序列化为 UCS-2 字符串,但这实际上是一种有损转换。

UTF-16作为字节的支持非常好: 它支持从BOM检测字节序, 或在代码中显式选择字节序。它还支持产生有BOM和无BOM的输出。

还有一些更有趣的转换可能是缺失的。没有办法将UTF-16字节流或字符串反序列化成UTF-8字符串,因为UTF-8从未被支持为反序列化形式。

此外,狭窄/宽字符世界与UTF/UCS世界完全分离。旧式的狭窄/宽字符编码与任何Unicode编码之间都没有转换。

输入/输出库

可以使用I/O库使用上述描述的wstring_convert和wbuffer_convert工具读写Unicode编码的文本。我认为标准库的这部分不需要支持太多其他内容。

正则表达式库

我之前在Stack Overflow上讲过关于C++正则表达式和Unicode的问题。我不会在这里重复所有这些观点,但只是陈述一下C++正则表达式没有一级Unicode支持,这是最低限度,使它们可用而无需到处使用UTF-32。

就这样?

是的,就是这样。这是现有的功能。有很多Unicode功能是看不到的,如规范化或文本分割算法。

U+1F4A9。有没有办法在C++中获得更好的Unicode支持?

常见的选择:ICUBoost.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 和一个结合音符。


29
我读得越多,就越感觉对所有这些一无所知。我几个月前阅读了大部分内容,但仍感觉像重新发现整件事一样... 为了让我这个头脑有些困惑的人易于理解,所有关于 utf8everywhere 的建议仍然有效,是吗?如果我“只是”想让我的用户能够打开和写入文件,而不受其系统设置的影响,我可以要求他们输入文件名,将其存储在 std::string 中,然后一切都应该正常工作,即使在 Windows 上也是如此。抱歉再次问这个问题... - Uflex
6
你所能真正做的,就是将std::string视为二进制块。在一个合适的Unicode实现中,无论内部(因为它被深藏在实现细节中)还是外部编码都不重要(嗯,有点重要,你仍然需要有可用的编码器/解码器)。 - Cat Plus Plus
3
@Uflex 可能是一个选项。我不确定遵循你不理解的建议是否是个好主意。 - R. Martinho Fernandes
21
@graham.reeds 哈哈,谢谢,但我已经知道了。请检查“致谢”部分 ;) - R. Martinho Fernandes
2
无论你想如何存储它(但使用wchar_t会很愚蠢)。当然,除非这是一个恒等转换。整个过程感觉很糟糕。设计这个的人根本不知道自己在做什么,委员会还批准了它 :( - R. Martinho Fernandes
显示剩余22条评论

41

Unicode不受标准库支持(对于任何合理的支持意义而言)。

std::stringstd::vector<char>无异:它完全忽略了Unicode(或任何其他表示/编码),并将其内容简单地视为一个字节。

如果您只需要存储和连接,那么它可以很好地工作;但是,一旦您希望使用Unicode功能(码点数量,字形数量等),您就会感到不幸。

我知道的唯一综合库是ICU。C++接口源自Java接口,因此远非成语。


2
Boost.Locale怎么样? - Uflex
11
从您提供的链接页面中可以了解到:为了实现这一目标,Boost.Locale使用最先进的Unicode和本地化库:ICU - 国际Unicode组件。 - Matthieu M.
1
Boost.Locale支持其他非ICU后端,请参见此处:http://www.boost.org/doc/libs/1_53_0/libs/locale/doc/html/using_localization_backends.html - Superfly Jon
@SuperflyJon:没错,但是根据同一页的内容,非 ICU 后端对 Unicode 的支持“严重有限”。 - Matthieu M.

29
您可以安全地将UTF-8存储在std::string中(或者在char[]char*中),因为Unicode NUL(U+0000)在UTF-8中是一个空字节,这是空字节在UTF-8中唯一的出现方式。因此,根据所有C和C++字符串函数,您的UTF-8字符串将被正确终止,并且您可以使用C++ iostreams(包括std::coutstd::cerr,只要您的语言环境是UTF-8)。
但是,您无法使用std::string获取UTF-8中的代码点长度。 std::string::size()会告诉您以字节为单位的字符串长度,当您处于UTF-8的ASCII子集内时,它才等于代码点数。
如果您需要在编码点级别上操作UTF-8字符串(即不仅仅是存储和打印它们),或者如果您正在处理UTF-16,这可能会有许多内部空字节,那么您需要研究宽字符字符串类型。

4
std::string 可以很好地嵌入空字符,并被投放到 iostreams 中。 - R. Martinho Fernandes
3
完全是有意为之的。它完全不会破坏c_str(),因为size()仍然可以工作。只有那些无法处理嵌入空字符(像大多数C世界中的API)的不正常的API才会出现问题。 - R. Martinho Fernandes
2
嵌入的空值会破坏c_str(),因为c_str()应该返回数据作为空终止的C字符串---由于C字符串不能有嵌入式空值,这是不可能的。 - uckelman
4
不再如此。c_str() 现在仅返回与 data() 相同的内容,即全部内容。需要传入大小的 API 可以使用它。不需要传入大小的 API 无法使用它。 - R. Martinho Fernandes
6
c_str()data() 的轻微区别在于,c_str() 确保结果后面跟着一个 NUL 类似对象,而我不认为 data() 这样做。不过,看起来现在 data() 也会这样做了。(当然,对于消耗大小而不是从终止符搜索推断大小的 API,这是不必要的。) - Ben Voigt
显示剩余2条评论

8

C++11引入了几种新的字面值字符串类型(new literal string types),用于Unicode。

不幸的是,标准库对于非统一编码(如UTF-8)的支持仍然不好。例如,没有很好的方法来获取UTF-8字符串的长度(以代码点计算)。


那么,如果我们想支持非拉丁语言,我们是否仍需要使用std::wstring来处理文件名?因为新的字符串字面量在这里并没有真正帮助,因为字符串通常来自用户... - Uflex
7
@Uflex std::string 可以轻松存储 UTF-8 字符串,但是例如 length 方法返回的是字符串中字节的数量而不是代码点的数量。 - Some programmer dude
9
说实话,获取字符串的代码点长度并没有太多用处。例如,以字节长度为准确预分配缓冲区可以使用。 - R. Martinho Fernandes
2
UTF-8字符串中的代码点数量并不是一个非常有趣的数字:可以将ñ写成“LATIN SMALL LETTER N WITH TILDE”(U+00F1)(这是一个代码点),或者是“LATIN SMALL LETTER N”(U+006E)后跟着“COMBINING TILDE”(U+0303),这是两个代码点。 - Martin Bonner supports Monica
所有那些关于“你不需要这个,你不需要那个”的评论,比如“代码点数量不重要”之类的,对我来说听起来有点可疑。一旦你编写了一个解析器,它应该解析各种utf8源代码,那么它是否将“LATIN SMALL LETTER N” == “(U+006E)后跟'COMBINING TILDE'(U+0303)”考虑在内,就取决于解析器的规范。 - BitTickler

5
然而,有一个非常有用的库叫做tiny-utf8,它基本上是std::string/std::wstring的替代品。其目的在于填补仍然缺失的utf8字符串容器类的空白。
这可能是最舒适的处理utf8字符串的方式(即没有unicode规范化和类似的东西)。您可以舒适地操作码点,而您的字符串仍然以运行长度编码的char形式编码。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接