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,仅做了注释上的修改。