C ++ 11支持Unicode的程度如何?


183

我已经听说C ++ 11支持Unicode。关于此的几个问题:

  • C ++标准库对Unicode的支持程度如何?
  • 请问std::string该怎么办?
  • 如何使用?
  • 潜在的问题在哪里?

19
“ std :: string应该做什么吗?” 您认为应该怎么办?
R. Martinho Fernandes

2
我将utfcpp.sourceforge.net用于我的utf8需求。它是一个简单的头文件,为Unicode字符串提供了迭代器。
fscan

2
std :: string应该存储字节,即UTF-8编码的代码单元序列。是的,从一开始就做到了。utf8everywhere.org
Pavel Radzivilovsky 2013年

3
支持Unicode的最大潜在问题在于Unicode及其在信息技术中的使用。Unicode不适用于(也不是设计用于)Unicode。Unicode旨在重现某人在某个地方写的所有可能的字形,在某个时间重现所有可能的细微差别,包括3种或4种不同的含义以及3种或4种不同的构成同一字形的方式。它并不意味着可用于日常语言,也不意味着适用或易于或明确地进行处理。
达蒙

11
是的,它被设计用于日常语言。至少是我的。而你也很有可能也是。事实证明,以一般方式处理人类文本是一项非常困难的任务。甚至不可能明确定义字符是什么。一般字形的复制甚至都不是Unicode宪章的一部分。
Jean-Denis Muys 2013年

Answers:


267

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/ mbrtoc16c32rtomb/ 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_utf8codecvt_utf16codecvt_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无法编码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_convertwbuffer_convert功能,可以使用I / O库以Unicode编码读取和写入文本。我认为标准库的这一部分不需要其他很多支持。

正则表达式库

之前,我已经在Stack Overflow上阐述了C ++正则表达式和Unicode的问题。我在这里不会重复所有这些要点,而只是声明C ++正则表达式不具有1级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。

☦是的,我知道Eszett)的问题,但是即使您一夜之间将所有德国语言环境都更改为将ß变为upper,也有很多其他情况会失败。尝试使用大写U + FB00ʟᴀᴛɪʟᴀᴛsᴍᴀʟʟᴍᴀʟʟɪɢᴀᴛᴜʀᴇғғ。没有ʟᴀᴛɪɴᴄᴀᴘɪʟʟɪɢᴀᴛᴜʀᴇғғ;它只是大写到两个F。或U + 01F0ʟᴀᴛɪɴsᴍᴀʟʟᴡᴡɪᴛʜᴄᴀʀᴏɴ; 没有预先设定的资本;它只是大写的大写字母J和组合的caron。


26
我读得越多,就越有一种不了解这一切的感觉。几个月前,我阅读了其中大部分内容,但仍然觉得自己又重新发现了整个内容...为了简化我可怜的大脑的工作,现在这个问题有点痛了,关于utf8everywhere的所有这些建议仍然有效,对?如果我“只是”希望我的用户无论他们的系统设置如何都可以打开和写入文件,我可以问他们文件名,将其存储在std :: string中,即使在Windows上,一切也应该正常工作?很抱歉再次提出要求...
Uflex 2013年

5
@Uflex您真正可以使用std :: string做的就是将其视为二进制blob。在适当的Unicode实现中,内部(因为隐藏在实现细节中)和外部编码都无关紧要(嗯,排序,您仍然需要编码器/解码器可用)。
Cat Plus Plus

3
@Uflex也许。我不知道遵循您不理解的建议是否是个好主意。
R. Martinho Fernandes

1
在C ++ 2014/17中有关于Unicode支持的建议。但是那是1,可能是4年后,现在几乎没有用。open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3572.html
graham.reeds 2013年

20
@ graham.reeds哈哈,谢谢,但是我意识到了这一点。检查“致谢”部分;)
R. Martinho Fernandes 2013年

40

标准库不支持Unicode (出于所支持的任何合理含义)。

std::string没有更好的比std::vector<char>:它是完全无视的Unicode(或任何其它表示/编码)和简单地对待它作为内容的blob的字节。

如果只需要存储和分类blob,则效果很好。但是一旦您希望使用Unicode功能(代码点数量,字素数量等),您就会很不走运。

我所知道的唯一全面的库是ICU。尽管C ++接口是从Java派生的,所以它远不是惯用的。



11
@Uflex:从链接的页面开始为了实现此目标,Boost.Locale使用了最新的Unicode和本地化库:ICU-Unicode国际组件。
Matthieu M.

1
Boost.Locale支持其他非ICU后端,在这里看到:boost.org/doc/libs/1_53_0/libs/locale/doc/html/...
Superfly的乔恩·

@SuperflyJon:是的,但是根据同一页面,非ICU后端对Unicode的支持受到“严重限制”。
Matthieu M.

24

您可以UTF-8在安全存储std::string(或者在一个char[]char*,对于这个问题),由于是一个Unicode NUL(U + 0000)是UTF-8空字节,这是唯一的办法空字节可以出现在UTF-8中。因此,您的UTF-8字符串将根据所有C和C ++字符串函数正确终止,并且您可以使用C ++ iostream(包括std::coutstd::cerr,只要您的语言环境为UTF-8)将它们悬挂起来。

std::string对于UTF-8,您不能做的就是获得代码点的长度。std::string::size()会告诉您以字节为单位的字符串长度,该长度仅等于您位于UTF-8的ASCII子集中时的代码点数。

如果您需要在代码点级别上对UTF-8字符串进行操作(即,不仅存储和打印它们),或者正在处理UTF-16(它可能有许多内部空字节),则需要研究一下宽字符串类型。


3
std::string可以扔进带有嵌入式null的iostream中,就好了。
R. Martinho Fernandes 2013年

3
这完全是故意的。它根本不会中断c_str(),因为它size()仍然有效。只有损坏的API(即,像大多数C语言世界一样,不能处理嵌入式null的API)才会损坏。
R. Martinho Fernandes

1
嵌入的空值中断c_str()是因为c_str()应该以空值终止的C字符串形式返回数据-这是不可能的,因为C字符串不能具有嵌入的空值。
uckelman

4
不再。c_str()现在只需返回与相同的值data(),即全部返回。占用大小的API可以使用它。没有的API不能。
R. Martinho Fernandes

6
稍有不同,c_str()可以确保结果后跟一个类似NUL char的对象,但我认为data()不会。不,看起来data()现在也一样。(当然,对于使用大小而不是通过终止符搜索来推断大小的API而言,这不是必需的)
Ben Voigt

8

C ++ 11 为Unicode 提供了一些新的文字字符串类型

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


因此,如果我们要支持非拉丁语言,是否仍需要使用std :: wstring作为文件名?因为新的字符串文字在这里实际上没有帮助,因为字符串通常来自用户……
Uflex 2013年

7
@Uflex std::string可以毫无问题地容纳 UTF-8字符串,但是例如该length方法返回字符串中的字节数,而不是代码点数。
程序员伙计

8
老实说,获取字符串代码点的长度没有太多用处。例如,以字节为单位的长度可用于正确地预分配缓冲区。
R. Martinho Fernandes 2013年

2
UTF-8字符串中的代码点数量不是一个非常有趣的数字:可以写ñ为“带小标题的拉丁文小写字母N”(U + 00F1)(这是一个代码点)或“拉丁文小写字母N”( U + 006E)后跟“ COMBINING TILDE”(U + 0303),这是两个代码点。
马丁·邦纳

所有那些关于“您不需要这个,也不需要那个”的评论,例如“不重要的代码点数”等,对我来说似乎有点可疑。一旦编写了应该解析utf8源代码的解析器,则是否考虑LATIN SMALL LETTER N' == 取决于解析器的规范(U+006E) followed by 'COMBINING TILDE' (U+0303)
BitTickler

4

然而,有一个叫非常有用的库微小-UTF8,这基本上是一个简易替换std::string/ std::wstring。它旨在填补仍缺少的utf8字符串容器类的空白。

这可能是处理utf8字符串(即没有Unicode规范化和类似内容)的最舒适的方式。您可以轻松地对codepoint进行操作,而字符串则始终以run-length-encoded chars 进行编码。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.