189

我已经阅读并听说 C++11 支持 Unicode。对此有几个问题:

  • C++ 标准库对 Unicode 的支持程度如何?
  • std::string它应该做的事吗?
  • 我该如何使用它?
  • 潜在问题在哪里?
4

5 回答 5

275

C++ 标准库对 unicode 的支持程度如何?

可怕。

快速浏览一下可能提供 Unicode 支持的库设施,我得到了这个列表:

  • 字符串库
  • 本地化库
  • 输入/输出库
  • 正则表达式库

我认为除了第一个之外,所有的都提供了可怕的支持。在快速绕过您的其他问题后,我将更详细地讨论它。

std::string它应该做的事吗?

是的。根据 C++ 标准,这是std::string和它的兄弟应该做的:

类模板basic_string描述的对象可以存储由不同数量的任意类似字符的对象组成的序列,其中序列的第一个元素位于零位置。

嗯,std::string这样做就好了。这是否提供任何特定于 Unicode 的功能?不。

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

我该如何使用它?

将其用作char对象序列;假装它是别的东西注定会以痛苦告终。

潜在问题在哪里?

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

字符串库

字符串库为我们提供了basic_string,它只是标准所谓的“类似字符的对象”的序列。我称它们为代码单元。如果您想要一个高级的文本视图,这不是您想要的。这是适合序列化/反序列化/存储的文本视图。

它还提供了 C 库中的一些工具,可用于弥合狭义世界和 Unicode 世界之间的差距:c16rtomb/mbrtoc16c32rtomb/ 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 进行简单的大小写转换touppertolower例如,对于某些德语语言环境来说,这还不够好:“ß”大写为“SS”☦,但toupper只能返回一个字符代码单元。

接下来,wstring_convert/wbuffer_convert和标准代码转换方面。

wstring_convert用于将一种给定编码的字符串转换为另一种给定编码的字符串。此转换涉及两种字符串类型,标准称为字节字符串和宽字符串。由于这些术语确实具有误导性,因此我更喜欢分别使用“序列化”和“反序列化”†。

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

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

该标准提供了一些 codecvt 类模板供这些工具使用:codecvt_utf8codecvt_utf16codecvt_utf8_utf16和一些特化codecvt。这些标准方面一起提供了以下所有转换。(注意:在下面的列表中,左边的编码总是序列化的 string/streambuf,右边的编码总是反序列化的 string/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>where sizeof(wchar_t) == 4;
  • UTF-16 ↔ UCS-2 ,其中codecvt_utf16<char16_t>, 和codecvt_utf16<wchar_t>where sizeof(wchar_t) == 2;
  • UTF-16 ↔ UTF-32 与codecvt_utf16<char32_t>, 和codecvt_utf16<wchar_t>where sizeof(wchar_t) == 4;
  • UTF-8 ↔ UTF-16 带有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_convertwbuffer_convert工具读取和写入 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。

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

于 2013-06-14T10:07:52.727 回答
41

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

std::string不比std::vector<char>: 它完全忽略了 Unicode(或任何其他表示/编码),只是将其内容视为字节块

如果您只需要存储和连接blob,它就可以很好地工作;但是,一旦您希望使用 Unicode 功能(代码点数、字素数等),您就不走运了。

我所知道的唯一综合图书馆是ICU。尽管 C++ 接口是从 Java 接口派生的,但它远非惯用的。

于 2013-06-14T09:28:05.567 回答
26

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

对于 UTF-8 ,你不能做std::string的是获取代码点的长度。std::string::size()将告诉您以字节为单位的字符串长度,当您在 UTF-8 的 ASCII 子集中时,它仅等于代码点的数量。

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

于 2013-06-14T08:34:45.497 回答
8

C++11 有几个新的 Unicode 文字字符串类型

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

于 2013-06-14T08:14:38.097 回答
4

但是,有一个非常有用的库叫做tiny-utf8,它基本上是/的替代品。它旨在填补仍然缺失的 utf8-string 容器类的空白。std::stringstd::wstring

这可能是“处理” utf8 字符串的最舒适的方式(即,没有 unicode 规范化和类似的东西)。您可以轻松地对codepoints进行操作,而您的字符串仍以 run-length-encoded chars 进行编码。

于 2016-11-30T15:20:18.890 回答