65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 C++ 流读取 UTF-8

2009 年 7 月 17 日

CPOL

24分钟阅读

viewsIcon

429107

downloadIcon

5312

一个从 char 到 wchar_t 的 locale codecvt facet。

 

介绍 

本文旨在讲解如何使用 UTF-8 编码读取和写入 Unicode 到字符流。因此,本文也涉及 C++ STL/Iostream 库一个常被误解的方面:区域设置。

STL 自带的文档,尽管技术上完美,但对于理解即使是像a_stream >> variable;这样简单的表达式中涉及的对象之间的关系,帮助不大,部分原因是一些细节被底层逻辑隐藏了。

此外,STL 的行为以及与操作系统的关系并非总是那么明显,有时会使某些操作变得“神秘”。

本文将深入探讨 Unicode 编码、STL 区域设置以及与 Windows 的关系。

我们所指的代码旨在支持 VC8(我使用 C++ 2005 Express)以及 MINGW(该编译器如何分发 Unicode 远非显而易见)。

摘要

  1. 引言 
  2. 关于 Unicode 的一点知识
    1. 最初
    2. 代码页的到来
  3. Unicode
    1. 流行的 Unicode 编码
      1. UTF-8 编码方案
      2. UTF-16 编码方案
  4. Windows 和 Unicode
    1. Windows 和本地化
  5. C 和 Unicode
  6. C++ 和 Unicode
    1. 流缓冲区和区域设置
  7. 转向 UTF-8
    1. MinGW 声明
    2. gel::stdx::utf8cvt<bool>
      1. 无效字符
      2. 微不足道的函数
      3. do_in
      4. do_out
  8. 使用 facet
  9. 提供的代码
    1. 测试序列
    2. 一个实际的例子
    3. MinGW 和 VC8 的其他显著差异
  10. 何时使用

关于 Unicode 的一点知识

字符编码的历史并非像它可能的那样线性:在特定时间做出的一些假设后来被推翻,这造成了——并且仍在造成——一些混乱。

最初

最初有打字机(在纸上打字的机械机器)和电传打字机,它们本质上是键盘和纸之间有电线的打字机。

为了确保这些机器两部分之间的互操作性,ANSI 委员会定义了 ASCII 字符集。

该集合旨在为 26 个拉丁字符提供二进制表示形式,包括小写和大写格式、一些标点符号和重音符号,以及电传打字中常用的一些“命令”,如 CR、LF、FF 等。它被设计为使用 7 位,以帮助硬件制造商将第 8 位用于错误检查(奇偶校验)。

缺少重音符号并不是什么大问题,因为电传打字机可以重叠打印:“à”只需写成“a BS '”(BS 是退格键)

不一定需要支持不同的国家:ASCII 是“美国标准代码...”。非美国人(或不习惯美国标准的人)只需使用另一种编码方案。

交互式计算机(带键盘和显示器)使这方面的问题变得复杂了一段时间

  • 软件的流通不再是国家限制的事情了,而且……
  • 显示器的性质要求字符矩阵将代表给定位置显示的“字符”的代码关联起来,因此……
  • 像使用退格键来获取组合字符这样的技巧不再有效。

硬件制造商首先尝试扩展 ASCII 字符集。

IBM 在推出第一台 PC 时,提供了一个 8 位字符集,包含 256 个符号,其中 32 到 126 与 ASCII 字符匹配(“ASCII 可打印字符”),并添加了一些带重音的字母、一些数学符号、一些半图形字符。

所有这些“一些”都是折衷的结果,实际上并没有满足每个人的需求:它只是试图满足当时 90% 的用户或 IBM 服务国家 90% 的用户。但它适合 8 位。

代码页的到来

为了更好地解决这个问题,引入了代码页的概念。
本质上,代码字形之间的对应关系变得可配置,以便每个国家都可以用其最需要的字符配置字符集的后半部分。互操作性仅由前 128 个代码保证。

后来的 DOS 版本和早期的 Windows 使用 8 位 ANSI 代码,并为各种“版本”提供了许多代码页。

这种方法的缺点是,它基本上不可能处理混合不同语言的非常异构的文本:混合阿拉伯语和日语实际上是不可能的。

而且,用一台阿拉伯语电脑阅读一篇法语文本有时是一种乐趣,甚至法语到意大利语也会导致奇怪的错误书写,因为相同的重音字符具有不同的编码。

此外,对于需要超过 128 个特定符号的语言(想想中文),问题仍然存在:为此引入了多字节代码页,产生了 MBCS。

Unicode

Unicode 的引入主要是为了清理所有这些混乱:它假设世界无法容纳 8 位,因此为每个编码符号提供了独特的 ID。

这被称为 UCS - 通用字符集。

在其最初的定义中,它包含的字符少于 65536 个,这使得许多软件开发人员确信 16 位足以表示所有字符。
这被称为 UCS-2。

目前的情况是 UCS 定义到 0x10FFFF(尽管仍有许多未分配的元素),因此需要 21 位。

UCS-4(4 字节)使用unsigned int(通常typedefdchar_t)作为字符,当然可以容纳所有内容,但对于许多语言来说,这会浪费空间。

此外,许多通信通道传输的是字节,而不是shortint,并且字节在shortint中的排序方式取决于处理器和机器的体系结构,因此,对于旨在在不同机器或设备之间进行通信或互操作的文件,纯二进制转储dcharswchars是不可行的。

流行的 Unicode 编码

为了解决上述问题,已经为 7、8、16 和 32 位环境部署了许多编码,试图保持与传统环境的互操作性。

特别是在 Windows 环境中,Unicode 最初部署为 UCS-2(16 位),通信仍然以字节为单位工作,8 位和 16 位尤其有用和方便。

这些编码被称为 UTF-8 和 UTF-16。

  • 前者在文件中很重要,因为需要与非 Windows 环境互换,因为它不依赖于机器的字节序。
  • 后者很重要,因为它取代了 Win32 API 内部的 UCS-2,通过使用“多字”方案来支持不适合前 655536 个代码点的字符。(请注意,UCS-2 和 UTF-16 不同:它们仅在前 65536-2048 个代码点上重合)。

UTF-8 编码方案

用于将 Unicode 表示为字节的编码基于定义如何将表示 UCS 的位串分解为字节的规则。

  • 如果 UCS 适合 7 位,则编码为 0xxxxxxx。这使得 ASCII 字符由其本身表示。
  • 如果 UCS 适合 11 位,则编码为 110xxxxx 10xxxxxx。
  • 如果 UCS 适合 16 位,则编码为 1110xxxx 10xxxxxx 10xxxxxx。
  • 如果 UCS 适合 21 位,则编码为 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx。
  • 如果 UCS 适合 26 位,则编码为 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx。
  • 如果 UCS 适合 31 位,则编码为 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx。

就实际的 UCS 空间而言,不应存在超过 21 位的编码,因此最后两条规则没有实际应用,实际上,当前的 Unicode 规范认为它们是无效的。

显然,这是一种偏爱低代码(导致更短的编码)而不是高代码的编码。
维基百科有一篇关于 UTF-8 的好文章,展示了与 UCS-4 和 UTF-16 之间的权衡。

但是,必须考虑到用于在页面中表示文本(想想 HTML)或在消息中表示数据(想想 XML)的所有标记都是 ASCII。
这可能平衡了例如中文文本字符串的最长编码。

此外,字节序无关紧要,因为所有代码都是“字节”,无需定义顺序。

由于这些原因,UTF-8 作为一种在互联网上存储文本的格式而流行起来,因为它们无论谁读写都保持不变。

UTF-16 编码方案

该编码是在 UCS-2 定义之后引入的,而 UCS-2 又是在发现 16 位不足以编码所有内容之后,对 16 位以下的 UCS 进行“原样”表示。

UTF-16 本质上利用了 UCS 中一个未分配的“区域”(从 0xD800 到 0xDFFF)来表示无法容纳在 16 位中的内容。
当然,人们强烈怀疑,这个未分配的区域是在发现没有空间来编码正在编码的内容之后才留下的。

本质上,字符编码如下:

  • 如果 UCS 适合 16 位,则编码为其本身(请注意,必须完全避免上述范围内的代码)
  • 如果 UCS 适合 21 位,则减去 0x10000(因此得到 20 位),并将结果分解为两个 10 位序列,分别与 0xD800(最高有效位)和 0xDC00(最低有效位)进行或运算。

值得注意的是,UTF-8 可以比实际的 Unicode 更宽(最多可达 31 位),但 UTF-16 的最大值固定为 21 位。当 Unicode 超过实际指定的 21 位时(如果真的会发生),看看会有什么创新将会很有趣……

Windows 和 Unicode

Windows 操作系统从最初的 IBM 字符集(或者更确切地说,OEM 字符集,因为不同的制造商可能有所不同)演变为 ANSI 字符集和代码页。

这指的是 8 位字符和依赖于语言的编码,主要用于 Win16。

随着 Win32 的到来,Unicode 首先被纯粹地采纳为 UCS-2,然后扩展以支持 UTF-16 代理。

操纵字符的 API 二进制文件被复制并重命名,通过添加“A”表示“ANSI”和“W”表示“宽”(例如MessageBoxAMessageBoxW),前者接受基于char的参数,后者接受基于wchar_t的参数。

然后,在<tchar.h>中定义了一些预处理器“魔术”,其中根据 UNICODE 和 MBCS 预处理器符号的定义,将传统的 API 名称映射到相应的AW

Windows 和本地化

为了处理不同国家和文化在表示数字、日期、货币等方面可能存在的差异,Windows 引入了“区域设置”的概念,作为一组可以通过 API 检索的信息,并且用户可以自定义,以帮助程序适应用户的习惯。  

不幸的是,这有时会导致误导,不仅是文本,连结构化数据也在通信和存储介质上以本地化形式表示,从而导致日期解释错误等问题(11/10 是什么日期……或者应该是 10/11?)。

所有这些信息都存储在系统注册表中。操作系统为各个国家提供了一组默认值,但用户可以通过提供自己的规范来覆盖它们。

例如,意大利使用“.”作为千位分隔符,使用“,”作为小数分隔符。
然而,意大利用户经常将“.”替换为“'”,以便数字 10'000 不容易出现读取错误,尤其是在不清楚其来源的情况下(因此……它可能只是 10)。

在 Windows 内部,UTF-8 到 16 和 16 到 8 的转换可以通过WideCharToMultiByteMultiByteToWideChar函数实现,方法是将CP_UTF8指定为代码页参数。

然而,它是一种字符串到字符串的转换,而不是流的编码/解码。

C 和 Unicode

C 语言完全早于 Unicode 和 Windows,实际上,它不提供任何对 Unicode 的直接支持,但许多库函数已被修改以处理国际化。

在这种环境中,许多面向字符的函数,如atof,都获得了相应的_wtof,并且——使用与<tchar.h>相同的预处理器魔法——_ttof被定义为其中一个,具体取决于UNICODEMBCS符号的定义。

charwchar_t实际代表什么取决于所使用的代码页,而代码页又与“区域设置”一起定义了数字的表示方式和字符的编码方式。

不幸的是,C 库的实现方式是基于一组静态数据定义“区域设置”特性,可通过set_locale函数选择。
这些数据与操作系统“区域设置”概念提供的数据无关,并且不可“用户自定义”(想想千位分隔符被替换为“'”的情况)。但是,可以通过mbcstowcs函数将 UTF-8 字符串转换为 UTF-16,并指定具有 UTF-8 代码页的区域设置。

这远比说起来要困难得多,因为库文档在这些类型的信息上并不那么慷慨。

例如,您可以发现在fopen中可以指定编码:例如。

fopen("newfile.txt", "rw, ccs=<encoding>");
 

其中<encoding>可以是“UTF-8”,尽管它没有被记录为标准。
但是当你转到 C++ 时,在fstream中几乎不可能重新找到类似的功能。

C++ 和 Unicode

C++ 的 I/O 方法基于“流”概念。流与文件之间的关系并不那么明显,因为 STL 文档没有提供清晰的描述。你需要阅读大量不同类的详细信息,才能对背后的体系结构有所了解。

那么让我们尽可能详细地讲解,以理解其中的线索

流缓冲区和区域设置

这些类在进行输入输出时扮演着协同角色。

  • 流负责提供插入和提取运算符以及格式化操纵器的接口。
  • 缓冲区负责为从外部读取和写入的元素提供临时存储,并管理这些读取和写入。
  • Locale 提供处理“表示”和“转换”的功能。它们通过许多“facet”来完成此操作,这些 facet 处理数字到字符串的转换(使用numgetnumput facet)以及字符从程序表示到外部表示的转换(使用codecvt facet)。

所有这些都是管理不同字符表示(charwchar_t)和不同外部流性质(文件或字符串)的系列类。

在它们的抽象定义中,流的根源是一个虚拟的ios_base(与字符类型无关),然后是basic_istream<.>basic_ostream<.>,再从这两个派生出basic_iostream<.>,从而形成以下层次结构:

所有都必须包含一个派生自basic_streambuf<.>的“缓冲区”和一个locale,默认初始化为 C++ 全局区域设置(又初始化为经典的“C”区域设置)。

特别是文件流,它们不过是使用basic_filebuf<.>初始化的基本流,该流重写了basic_streambuf<.>的虚函数来管理文件 I/O,并附加了一些直通函数,如openclose等。
类似地,字符串流是使用basic_stringbuf<.>初始化的基本流。

模板参数定义了流在程序内部使用的“元素”类型。在 Windows 环境中,它们通常是用于 ANSI 字符表示的char和用于 Unicode (UTF-16) 字符表示的wchar_t

但是文件流发生了一些奇怪的事情:试试这个

#include <fstream>
#include <windows.h>
#pragma comment(lib, "kernel32.lib")
#pragma comment(lib, "user32.lib")
#pragma comment(lib, "gdi32.lib")

int main()
{
 std::wofstream fs("testout.txt");
 const wchar_t* txt = L"some Unicode text òàè逧";
 MessageBoxW(0,txt,L"verify",MB_OK);
 fs << txt << std::flush;
 MessageBoxW(0,fs.good()? L"Good": L"Bad",L"verify",MB_OK);
 return 0;
}

UnicodeMessageBoxW的调用确认了正确的字符串(应该以§符号结尾,并以欧元符号作为倒数第二个字符)。

这是调试器中txt的转储

0x0041770C 73 00 6f 00 6d 00 65 00 20 00 55 00 6e 00 69 00 s.o.m.e. .U.n.i.
0x0041771C 63 00 6f 00 64 00 65 00 20 00 74 00 65 00 78 00 c.o.d.e. .t.e.x.
0x0041772C 74 00 20 00 f2 00 e0 00 e8 00 e9 00 ac 20 a7 00 t. .ò.à.è.é.¬ §.

那是 UTF-16 LE 形式表示的 Unicode (73-00,在WORD格式中是 0x0073,它只是普通的 0x73 ('s') ASCII,而 AC-20 是 0x20AC,即欧元符号 €,它不能表示为单个字节)。

现在用十六进制编辑器查看输出文件内容。

您应该会得到(我使用了带 Hexedit 插件的 Notepad++)

"000000000 73 6F 6D 65 20 55 6E 69-63 6F 64 65 20 74 65 78 |some Unicode tex|"
"000000010 74 20 F2 E0 E8 E9 |t òàèé |"

那是 ANSI,文本被 € 符号截断(并且fs.good()为 false)。

这至少在 VC8 中是这样。

用另一个编译器(MinGW 3.4.5,我用 Codelite 作为 IDE)对同一源文件进行相同的测试(注意 MinGW 使用 UTF-8 作为源文件,而 VC8 使用 ANSI。字符串字面量不会保留,因此需要不同的文件),情况甚至更糟。

t1.cpp: In function `int main()':
t1.cpp:9: error: `wofstream' is not a member of `std'
t1.cpp:9: error: expected `;' before "fs"
t1.cpp:10:23: converting to execution character set: 
t1.cpp:12: error: `fs' was not declared in this scope

实际上,所有与wchar_t相关的代码都在由_GLIBCXX_USE_WCHAR_T符号驱动的条件编译下。

引入此变通方法(本质上是在常规std命名空间中定义缺失的类型)后,它就可以编译了。

#ifdef __MINGW32_VERSION
#ifndef _GLIBCXX_USE_WCHAR_T

namespace std
{
	typedef basic_ios<wchar_t>           wios;
	typedef basic_streambuf<wchar_t>     wstreambuf;
	typedef basic_istream<wchar_t>       wistream;
	typedef basic_ostream<wchar_t>       wostream;
	typedef basic_iostream<wchar_t>      wiostream;
	typedef basic_stringbuf<wchar_t>     wstringbuf;
	typedef basic_istringstream<wchar_t> wistringstream;
	typedef basic_ostringstream<wchar_t> wostringstream;
	typedef basic_stringstream<wchar_t>  wstringstream;
	typedef basic_filebuf<wchar_t>       wfilebuf;
	typedef basic_ifstream<wchar_t>      wifstream;
	typedef basic_ofstream<wchar_t>      wofstream;
	typedef basic_fstream<wchar_t>       wfstream;
}

#endif
#endif

但运行它仍然显示fs出现bad,并且没有产生输出(文件已创建,但仍为空)。

调试显示,basic_streambuf::xsputn函数捕获了一个异常,并将流设置为bad。该异常在此处产生:

 template<typename _Facet>
 inline const _Facet&
 __check_facet(const _Facet* __f)
 {
 if (!__f)
 __throw_bad_cast();
 return *__f;
 }

其中_Facet的实际类型是std::codecvt<wchar_t,char,int>char?!它从何而来?。

回到 VC,我们在basic_filebuf(文件流的basic_streambuf派生类)文档中发现了这个奇怪的注释:

无论类型参数 Elem 指定的char_type是什么,basic_filebuf类型的对象都会使用char *类型的内部缓冲区创建。这意味着 Unicode 字符串(包含wchar_t字符)在写入内部缓冲区之前将被转换为 ANSI 字符串(包含char字符)。要将 Unicode 字符串存储在缓冲区中,请创建一个wchar_t类型的新缓冲区,并使用basic_streambuf::pubsetbuf()方法进行设置。有关演示此行为的示例,请参见下文。.

本质上,似乎没有人愿意清楚地说明——无论我们在程序中使用什么——输出到FILE(是的,旧的 C 语言FILE,那是basic_filebuf写入和读取的地方,背后没有什么魔法)时,默认情况下总是尝试转换为char,使用当前的全局区域设置 facets。  

在 VC8 中,这是通过默认全局区域设置的一部分std::codecvt<wchar_t,char,mbstate_t> facet 来实现的,它作用于一个内部字符缓冲区(如注释所述),而在 MINGW 中没有声明此类 facet,因此区域设置无法提供它(因此会引发异常)。

转向 UTF-8  

……这就是转向 UTF-8 的关键:让我们为basic_filebuf提供它想要的 facet。

不是另一个具有正确类型和 ID 的 facet(区域设置可以支持任意数量的“facet”),因为这并不是缓冲区类所寻找的。

我们必须派生出正确的std::codecvt<wchar_t,char,mbstate_t>,并且在没有定义此类类型的情况下,我们必须定义它。

MinGW 声明

如果我们假设 MinGW 未启用wchar_t支持,我们必须让基本 facet 存在。

由于所有 facet 都定义为模板,这很容易:

#ifdef __MINGW32_VERSION
#ifndef _GLIBCXX_USE_WCHAR_T

namespace std
{
	template<>
	class codecvt<wchar_t,char,mbstate_t>:
		public __codecvt_abstract_base<wchar_t,char,mbstate_t>
	{
	protected:
		explicit codecvt(size_t refs=0)
			:__codecvt_abstract_base<wchar_t,char,mbstate_t>(refs)
		{}
	public:
		static locale::id id;
	};

	typedef basic_ios<wchar_t> 			wios;
	typedef basic_streambuf<wchar_t> 		wstreambuf;
	typedef basic_istream<wchar_t> 			wistream;
	typedef basic_ostream<wchar_t> 			wostream;
	typedef basic_iostream<wchar_t> 		wiostream;
	typedef basic_stringbuf<wchar_t> 		wstringbuf;
	typedef basic_istringstream<wchar_t>		wistringstream;
	typedef basic_ostringstream<wchar_t>		wostringstream;
	typedef basic_stringstream<wchar_t> 		wstringstream;
	typedef basic_filebuf<wchar_t> 			wfilebuf;
	typedef basic_ifstream<wchar_t> 		wifstream;
	typedef basic_ofstream<wchar_t> 		wofstream;
	typedef basic_fstream<wchar_t> 			wfstream;
}

#endif
#endif

我们正在为<wchar_t,char,mstate_t>定义codecvt<InnerType,OuterType,StateType>的特化,这正是编译器所寻找的。

并且我们提供了一个locale::id静态对象,这是 STL 实现所要求的。这需要一个 cpp 文件来实例化静态对象(<rant>我讨厌全局变量...</rant>)。

此时,我们可以——对于 MinGW 和 VC8——派生codecvt<InnerType,OuterType,StateType>,重写虚函数以实现wchar_tchar的转换,其中wchar_t是 UTF-16,char是 UTF-8。

gel::stdx::utf8cvt<bool>

它是codecvt的派生类,实现为 UTF-16 <-> UCS <-> UTF-8 转换器,使用mbstate_t参数作为函数调用之间的携带。

无效字符

首先,我们必须决定在遇到无效字符时该怎么做:输入中可能存在但不是有效 UTF 或甚至有效 Unicode 码点的序列。

根据 Unicode 规范,无效字符或序列必须被视为“错误”。但“处理”意味着什么则留给了许多解释。

如果我们的目的是验证输入,我们可能会喜欢一些能让我们意识到出了问题的东西;但如果我们只是阅读文本,我们可能更倾向于一些不会因为一个写错的字符就停止阅读的东西。

这就是bool模板参数的作用:如果设置为true,实现将采用严格行为,并在每次读取或写入错误或非法序列或字符时抛出gel::stdx::utf_error异常,该异常派生自std::runtime_error

STL 实现的basic_streambuf应该捕获此异常并将所有者流设置为“bad”,从而阻塞它。因此,该逻辑将与常规流处理中通常使用的逻辑没有区别。

如果bool参数设置为 false,则 Unicode 限制的遵守放宽,无效序列将根据算法进行一致处理。
因此,可以支持多达 28 位的码点(我们需要 4 位来管理转换步骤),并像“合法”一样读取超长 UTF-8 序列。

微不足道的函数

对于至少三个函数来说,重写codecvt是微不足道的。

  • do_always_noconv总是返回 false,因为需要进行转换。
  • do_max_length返回 6,因为这是 UTF-8 的最长可能性。适当的 Unicode 永远不会产生超过 4 个char,但任意的wchar_t可能会超过。
  • do_encoding总是返回 -1,因为转换是依赖于状态的。

do_length则更复杂:需要几乎解码才能理解转换序列的长度。我们发现返回一个谨慎值(min(_Len2, (size_t)(_Last1-_First1))更简单。结果可能是filebuffrer类分配了更大的缓冲区,但——根据实验——MinGW 和 VC8 缓冲区实现似乎都没有调用此函数。

do_in

UTF-8(外部)到 16(内部)的转换是通过使用_Next1_Next2作为迭代器,从_First1_First2遍历到_Last1_Last2(难道不能不同吗?),每读取或写入一些内容就递增。如果到达缓冲区末尾,如果存在剩余状态,则函数返回partial,否则返回ok

_State值的使用如下:

  • 低 28 位存储输入的字符(Unicode 需要 21 位,因此我们可以处理比实际 Unicode 更多的字符)。
  • 位 28-29-30 用于存放构成一个字符的剩余char的计数器。这是 UTF-8 到 UCS 转换的进位。
  • 位 31 用作 UCS 到 UTF-16 转换的进位(在代理项的情况下)

函数的第一部分(在if(!(_Sate & 0x80000000))下)执行 UTF-8 到 UCS 的转换(if语句防止正在进行的 UCS 到 UTF-16 转换)。

第二部分(在if(!(_State & 0x70000000))下)在没有 UTF-8 残余时执行(因此_State & 0x0FFFFFFF包含一个完整的 UCS),并将 UCS 保存为 16 位或 21 位代理(10 + 10 位 + 0x100000),通过保存第一部分,设置_State位 31,并根据该设置,在下一个循环中保存第二部分。

请注意,“下一个循环”的概念取决于 STL 实现内部basic_streambuf所采用的策略。

它可能在主for循环内部,也可能在函数的进一步调用时。由于我们使用_State作为循环之间的进位,并且状态在函数外部分配和维护(由调用者传递),我们基本上不需要关心缓冲区剩余长度。

do_out

UTF-16(内部)到 8(外部)的转换类似。

_State位 31 用作进位标志,指示部分读取/写入。

当一个完整的 UCS 被读取(从 UTF-16)后,第一个 UTF-8 字节被保存。后续字节通过调用unshift写入,当没有更多输入时,basic_streambuf本身也会调用它来完成输出。

使用 facet

我们需要一个std::locale,用gel::stdx::utf8cvt替换其中的codecvt<wchar,char,mbstate_t> facet,并将其imbue到流缓冲区中。

这个非常神秘的断言,仅仅意味着这个:

 std::locale utf8_locale(std::locale(), new gel::stdx::utf8cvt<true>);
 std::wfstream fs;
 fs.imbue(utf8_locale);
 fs.open(yourfile, mode);
 //whatever I/O to the stream

在这种情况下,utf8_locale是从全局区域设置中获取的,并赋予了它一个utf8cvt

这个可以是任何wstream(在这个例子中是wfstream,即basic_fstream<wchar_t>)。  

当然,除了 imbue,我们还可以在utf8_locale声明之后立即调用以下函数来替换全局 locale:

 std::locale::global(utf8_locale); 

就在utf8_locale声明之后。

在适当的情况下,我们还可以使用从另一个特定区域设置中获取的区域设置,只需像这样创建一个区域设置即可:

 std::locale utf8_it(std::locale("It"), new gel::stdx::utf8cvt);

从而得到一个 UTF-8 意大利语区域设置。

由于 UTF-8 中性区域设置的频繁使用,我将gel::stdx::utf8_locale<true>声明为一个始终可访问的全局对象。

提供的代码

“gel”目录包含正确实例化全局变量和静态变量所需的头文件(“stdutif.h”)和 cpp 源文件(“gel.cpp”)。

父目录包含一个测试程序(“tester.cpp”)以及用于组织 VS8 项目和 Codelite 项目(也用于测试 MinGW 编译)的所有内容,祖父目录包含 VS8 解决方案以及 Codelite 工作区。

测试程序是一个控制台应用程序,它接受命令行中最多两个文件名(第一个作为输入文件,第二个作为输出文件)。
输入的默认值为testerin.txt,输出的默认值为testerout.txt

此应用程序执行多项关于使用 UTF-8 facet 进行读写的符合性测试,将其活动记录到标准输出 (std::cout),并使用 ANSI 和 Unicode Win32 API MessageBox[A/W] 测试读取文件的显示方式。

测试序列

测试程序按如下步骤进行:

  1. 文件作为std::ifstream(因此是基于char的)读入std::string,并通过MessageBoxA显示。
    由于这都是 ANSI,因此 UTF-8 只有 ASCII 兼容字符才能正确显示。
  2. 文件作为std::wifstream(因此是基于wchar_t的)读入std::wstring,使用正常的区域设置,并通过MessageBoxW显示。
    这将被 VC8 视为 ANSI 到 Unicode 的转换,而从 MinGW 看来,这是一个“无法读取”的流。
  3. 文件作为用gel::stdx::utf8_locale注入std::wifstream读取,并通过MessageBoxW显示,然后将输出文件作为用gel::stdx::utf8_locale注入std::wofstream写入,内容就是刚刚读取的字符串。
  4. 这两个文件都被作为原始字节序列(如std::ifstream)读取,并逐字节进行比较,以检查是否存在差异。

一个实际的例子

作为示例,在测试器项目目录中提供了文件“testerin.txt”,内容如下:

Tester text:
grade paragraph eacute egrave euro
°§èé€

该文件已使用 Notepad++ 创建并保存为无 BOM 的 UTF-8 格式。

运行测试程序,
步骤 1 完成后出现以下消息框:

step 1 message box

注意,读取并显示为 ANSI 的字节将 5 个 Unicode 表示为 2、2、2、2、3 个字节。

步骤 2 完成方式如下:

VC8 版本 MinGW 版本
step 2 as VC8
step 2 MinGW

请注意,MinGW 报告为空白显示,原因如文章所述(缺少默认的codecvt<wchar_t,char,mbstate_t>),而 VC8 则尝试进行 ANSI 到 Unicode 的转换。它保留了 ASCII 部分,但非 ASCII 的转换依赖于代码页(因此依赖于系统)。

步骤 3 完成如下:

step 3

这是 UTF-8 文件的正确显示。

步骤 4,此时检查正确读取的文件是否也通过比较两个字节序列正确写入。

step 4

整个执行过程在控制台中记录如下:

console output

(注意:如果输入文件和输出文件之间有任何不同,“=”将替换为“x”。
此外,请注意 UTF-8 读取如何比 ANSI 读取更短,因为 2 字节和 3 字节字符的压缩)。

MinGW 和 VC8 的其他显著差异

调试这两个版本时,do_in函数显示了 STL 两个实现的不同调用算法。

VC8 反复调用该函数,每次调用传递一个字节(因此反映了测试器循环中的while(fs.get(c)))。

MinGW 则一次调用该函数,提供整个字节序列(并让函数内部循环完成工作)。

何时使用

最后是一些基于我的观点和经验的建议。

  1. 这不是解决所有问题的万能药,也不是专业的实现代码。它只是想填补一个空白,我希望其他人能很快用更好的结构化代码来填补,甚至可能在 STL 实现本身内部。
  2. 是的,我知道 Boost 有这个功能。但这只有 10KB 长,而且 Boost 没有文档说明这个 facet(似乎它是因为作者的私人需求而被包含进来的,他需要它来做其他事情:你必须破解 Boost 代码才能发现它)。
  3. 这个 facet 当然会使你的代码变慢,因为转换会带来开销。如果你不需要理解流中包含的文本内容,而只是移动它,那么就不要转换它:只需将其视为“原始字节”。
    如果你必须解析 ASCII 分隔的标记(如 HTML 或 XML),而不需要与标记文本交互,那么就不要转换它。只需将 UTF-8 视为“假 ASCII”。
  4. 如果你需要获取一些 UTF-8 文本并将其传递给 Windows API 进行用户交互,那么是的,你必须将其转换为 Unicode,因为 ANSI 无法正确表示它。这可能是一种方法。另一种方法是将其作为原始字节读取,然后通过 MultiByteToWideChar(并指定 UTF-8 作为代码页)在内存中转换字符串。
  5. 这种方法(也是它被设计出来的原因)的理想情况是读/写相对较小的配置文件,这些文件保存为 UTF-8 格式,用于系统之间的互操作性,或者用于通过套接字进行简单的网络消息编码/解码。

© . All rights reserved.