8.3.3 关于Unicode的库支持

C++11在标准库中增加了一些Unicode编码转换的支持。由于char16_t及char32_t也是C11标准中新增的类型,所以C库及C++库均有一些不同的实现。

首先我们可以看一些比较直观的编码转换函数。在C11中,程序员可以使用库中的一些新增的编码转换函数来完成各种Unicode编码间的转换。函数的原型如下:


size_t mbrtoc16(char16_tpc16,const chars,size_t n,mbstate_t*ps);

size_t c16rtomb(chars,char16_t c16,mbstate_tps);

size_t mbrtoc32(char32_tpc32,const chars,size_t n,mbstate_t*ps);

size_t c32rtomb(chars,char32_t c32,mbstate_tps);


上述代码中,字母mb是multi-byte(这里指多字节字符串,后面会解释)的缩写,c16和c32则是char16和char32的缩写,rt是convert(转换)的缩写。代码中的几个函数原型大同小异,目的就是完成多字节字符串、UTF-16及UTF-32之间的一些转换。除了mbstate_t是用于返回转换中的状态信息外,其余部分意义比较明显,读者应该能直观理解它们的含义。代码清单8-16所示是一个可能通过编译的例子。

代码清单8-16


include <iostream>

include <cuchar>

using namespace std;

int main(){

char16_t utf16[]=u"\u4F60\u597D\u554A";

char mbr[sizeof(utf16)*2]={0};//这里我们假设buffer这么大就够了

mbstate_t s;

c16rtomb(mbr,utf16,&s);

cout<<mbr<<endl;

}

//编译选项:g++ -std=c++11-c 8-3-3.cpp


使用C11中编码转换函数需要include头文件<cuchar>。不过在本书写作的时候,我们使用的编译器都还没能提供这个头文件及其实现。所以代码清单8-16所示的例子仅供参考。

C++对字符转换的支持则稍微复杂一点,不过C++对编码转换支持的新方法都需要源自于C++的locale机制的支持[1]。事实上,locale的概念在POSIX中就用,在C++中,通常情况下,locale描述的是一些必须知道的区域特征,如程序运行的国家/地区的数字符号、日期表示、钱币符号等。比如在美国地区且采用了英文和UTF-8编码,这样的locale可以表示为en_US.UTF-8,而在中国使用简体中文并采用GB2312文字编码的locale则可以被表示为zh_CN.GB2312,等等。

通常知道了一个地区的locale,要使用不同的地区特征,则需访问该locale的一个facet。facet可以简单地理解为是locale的一些接口。比如对于所有的locale都会有num_put/num_get的操作,那么这些操作就是针对该locale数值存取的接口,即该locale情况下数值存取的facet。在C++中常见的facet除去num_get/num_put、money_get/money_put等外,还有一种就是codecvt。

codecvt从类型上来讲是一个模板类,从功能上讲,是一种能够完成从当前locale下多字符编码字符串到多种Unicode字符编码转换(也包括Unicode字符编码间的转换)的facet。这里的多字节字符串不仅可以是UTF-8,也可以是GB2312或者其他,其实际依赖于locale所采用的编码方式。在C++标准中,规定一共需要实现4种这样的codecvt facet[2]


std::codecvt<char,char,std::mbstate_t>//完成多字节与char之间的转换

std::codecvt<char16_t,char,std::mbstate_t>//完成UTF-16与UTF-8间的转换

std::codecvt<char32_t,char,std::mbstate_t>//完成UTF-32与UTF-8间的转换

std::codecvt<wchar_t,char,std::mbstate_t>//完成多字节与wchar_t之间的转换


每种facet负责不同类型编码数据的转换。值得注意的是,现行编译器支持情况下,一种locale并不一定支持所有的codecvt的facet。程序员可以通过has_facet来查询该locale在本机上的支持情况,如代码清单8-17所示。

代码清单8-17


include <iostream>

include <locale>

using namespace std;

int main(){

//定义一个locale并查询该locale是否支持一些facet

locale lc("en_US.UTF-8");

bool can_cvt=has_facet<codecvt<wchar_t,char,mbstate_t>>(lc);

if(!can_cvt)

cout<<"Do not support char-wchar_t facet!"<<endl;

can_cvt=has_facet<codecvt<char16_t,char,mbstate_t>>(lc);

if(!can_cvt)

cout<<"Do not support char-char16 facet!"<<endl;

can_cvt=has_facet<codecvt<char32_t,char,mbstate_t>>(lc);

if(!can_cvt)

cout<<"Do not support char-char32 facet!"<<endl;

can_cvt=has_facet<codecvt<char,char,mbstate_t>>(lc);

if(!can_cvt)

cout<<"Do not support char-char facet!"<<endl;

return 0;

}

//编译选项:g++ -std=c++11 8-3-4.cpp


编译运行代码清单8-17,在我们的实验机环境及编译器支持情况下,可以得到以下结果:


Do not support char-char16 facet!

Do not support char-char32 facet!


由上述结果可知,从char到char16或char32转换的两种facet还没有被支持(实验机使用的编译器尚未支持)。

而在使用facet上,用户并不需要显式地在代码中生成codecvt对象。比如在对C++11中stream进行I/O时,我们只需要一些简单的设定,就可以让stream自动进行一些编码的转换。我们看一下代码清单8-18所示的例子[3]

代码清单8-18


include <iostream>

include <fstream>

include <string>

include <locale>

include <iomanip>

using namespace std;

int main()

{

//UTF-8字符串,"\x7a\xc3\x9f\xe6\xb0\xb4\xf0\x9d\x84\x8b";

ofstream("text.txt")<<u8"z\u00df\u6c34\U0001d10b";

wifstream fin("text.txt");

//该locale的facet-codecvt<wchar_t,char,mbstate_t>

//可以将UTF-8转化为UTF-32

fin.imbue(locale("en_US.UTF-8"));

cout<<"The UTF-8 file contains the following wide characters:\n";

for(wchar_t c;fin>>c;)

cout<<"U+"<<hex<<setw(4)<<setfill('0')<<c<<'\n';

}

//编译选项:g++ -std=c++11 8-3-5.cpp


在代码清单8-18中,我们使用了wifstream来打开一个UTF-8编码的文件。随后调用了这个wifstream的imbue函数,为其设定了一个为en_US.UTF-8的locale。这样一来当进行I/O操作的时候,会使用完成UTF-8到UTF-32编码转换的facet(codecvt<wchar_t,char,mbstate_t>)来完成编码转换。编译运行代码清单8-18,我们就可以看到定义的Unicode字符串的十六进制表示。


The UTF-8 file contains the following wide characters:

U+007a

U+00df

U+6c34

U+1d10b


codecvt还派生一些形如codecvt_utf8、codecvt_utf16、codecvt_utf8_utf16等可以用于字符串转换的模板类。这些模板类配合C++11定义的wstirng_convert模板,可以进行一些不同字符串的转换。代码清单8-19也是一个C++11标准中的示例,不过由于我们编译器尚未支持,所以也仅供参考。

代码清单8-19


include <cvt/wstring>

include <codecvt>

include <iostream>

using namespace std;

int main(){

wstring_convert<codecvt_utf8<wchar_t>>myconv;

string mbstring=myconv.to_bytes(L"Hello\n");

cout<<mbstring;

}


除了to_bytes外,wstring_convert还支持使用from_bytes来完成逆向的编码转换。更多关于wstring_convert、locale、codecvt的内容,读者可以参看一些在线文档,这里不再展开描述。

此外,还有一点值得注意,在C++98标准定义wchar_t类型的时候,为其添加了新的fstream类型,如wifstream及wofstream等。不过C++11标准并没有为char16_t及char32_t再次产生fstream对象。关于这点,跟前面提到的UTF-8操作问题有类似。标准委员会意识到在Unicode在序列化存储时很少是UTF-16或者是UTF-32的(空间太过浪费)。所以从实际情况出发,程序员可以利用不同的codecvt的facet来将UTF-8编码存储的字符与不同的Unicode进行转换,而不必直接将UTF-16和UTF-32编码的字符存储到文件,基于此,也就没在C++11标准中提供支持该功能的u16ifstream、u32ofstream等。

事实上,尽管C++11对Unicode做了更多的支持,Unicode字符串的使用仍然比ASCII字符复杂。如我们所见的,程序在进行各种I/O操作时,往往需要UTF-8编码的字符。程序员如果想直接在内存中操作UTF-8编码字符,那么对UTF-8字符串的string进行遍历、插入、删除、查找等操作会比较困难。如果遇到这样的情况,程序员可以自行寻求一些第三方库的支持。

[1]可以参考该文理解C++的locale机制:http://www.cantrip.org/locale.html。

[2]参见http://en.cppreference.com/w/cpp/locale/codecvt。

[3]本例来源于http://en.cppreference.com/w/cpp/locale/codecvt,仅做了注释上的修改。