完全解密C++寬字符(二)html
轉:http://club.topsage.com/thread-2227977-1-1.htmlios
四、利用codecvt和use_facet轉換
locale和facet
C++ 的locale框架比C更完備。C++除了一個籠統本地策略集locale,還能夠爲locale指定具體的策略facet,甚至能夠用本身定義的 facet去改造一個現有的locale產生一個新的locale。若是有一個facet類NewFacet須要添加到某個old_loc中造成新 new_loc,須要另一個構造函數,一般的作法是:
std::locale new_loc(old_loc, new NewFacet);
標準庫裏的標準facet都具備本身特有的功能,訪問一個locale對象中特定的facet須要使用模板函數use_facet:
template <class Facet> const Facet& use_factet(const locale&);
換一種說法,use_facet把一個facet類實例化成了對象,由此就可使用這個facet對象的成員函數。
codecvt
codecvt就是一個標準facet。在C++的設計框架裏,這是一個通用的代碼轉換模板——也就是說,並非僅僅爲寬窄轉換制定的。
templat <class I, class E, class State> class std::codecvt: public locale, public codecvt_base{...};
I表示內部編碼,E表示外部編碼,State是不一樣轉換方式的標識,若是定義以下類型:
typedef std::codecvt<wchar_t, char, mbstate_t> CodecvtFacet;
那麼CodecvtFacet就是一個標準的寬窄轉換facet,其中mbstate_t是標準寬窄轉換的State。
內部編碼和外部編碼
咱們考慮第1節中提到的C++編譯器讀取源文件時候的情形,當讀到L"中文abc"的時候,外部編碼,也就是源文件的編碼,是GB2312或者UTF-8的 char,而編譯器必須將其翻譯爲UCS-2BE或者UTF-32BE的wchar_t,這也就是程序的內部編碼。若是不是寬字符串,內外編碼都是 char,也就不須要轉換了。相似的,當C++讀寫文件的時候 ,就會可能須要到內外編碼轉換。事實上,codecvt就正是被文件流緩存basic_filebuf所使用的。理解這一點很重要,緣由會在下一小節看到。
CodecvtFacet的in()和out()
由於在CodecvtFacet中,內部編碼設置爲wchar_t,外部編碼設置爲char,轉換模式是標準寬窄轉換mbstate_t,因此,類方法in()就是從char標準轉換到wchar_t,out()就是從 wchar_t標準轉換到char。這就成了咱們正須要的內外轉換函數。
result in(State& s, const E* from, const E* from_end, const E*& from_next, I* to, I* to_end, I*& to_next) const;
result out(State& s, const I* from, const I* from_end, const I*& from_next, E* to, E* to_end, E*& to_next) const;
其中,s是非const引用,保存着轉換位移狀態信息。這裏須要重點強調的是,由於轉換的實際工做交給了運行時庫,也就是說,轉換可能不是在程序的主進程中完成的,而轉換工做依賴於查詢s的值,所以,若是s在轉換結束前析構,就可能拋出運行時異常。因此,最安全的辦法是,將s設置爲全局變量!
const的3個指針分別是待轉換字符串的起點,終點,和出現錯誤時候的停點(的下一個位置);另外3個指針是轉換目標字符串的起點,終點以及出現錯誤時候的停點(的下一個位置)。
代碼以下:
頭文件編程
//Filename string_wstring_cppcvt.hpp #ifndef STRING_WSTRING_CPPCVT_HPP #define STRING_WSTRING_CPPCVT_HPP #include <iostream> #include <string> const std::wstring s2ws(const std::string& s); const std::string ws2s(const std::wstring& s); #endif
實現:緩存
#include "string_wstring_cppcvt.hpp" mbstate_t in_cvt_state; mbstate_t out_cvt_state; const std::wstring s2ws(const std::string& s) { std::locale sys_loc(""); const char* src_str = s.c_str(); const size_t BUFFER_SIZE = s.size() + 1; wchar_t* intern_buffer = new wchar_t[BUFFER_SIZE]; wmemset(intern_buffer, 0, BUFFER_SIZE); const char* extern_from = src_str; const char* extern_from_end = extern_from + s.size(); const char* extern_from_next = 0; wchar_t* intern_to = intern_buffer; wchar_t* intern_to_end = intern_to + BUFFER_SIZE; wchar_t* intern_to_next = 0; typedef std::codecvt<wchar_t, char, mbstate_t> CodecvtFacet; CodecvtFacet::result cvt_rst = std::use_facet<CodecvtFacet>(sys_loc).in( in_cvt_state, extern_from, extern_from_end, extern_from_next, intern_to, intern_to_end, intern_to_next); if (cvt_rst != CodecvtFacet::ok) { switch(cvt_rst) { case CodecvtFacet::partial: std::cerr << "partial"; break; case CodecvtFacet::error: std::cerr << "error"; break; case CodecvtFacet::noconv: std::cerr << "noconv"; break; default: std::cerr << "unknown"; } std::cerr << ", please check in_cvt_state." << std::endl; } std::wstring result = intern_buffer; delete []intern_buffer; return result; } const std::string ws2s(const std::wstring& ws) { std::locale sys_loc(""); const wchar_t* src_wstr = ws.c_str(); const size_t MAX_UNICODE_BYTES = 4; const size_t BUFFER_SIZE = ws.size() * MAX_UNICODE_BYTES + 1; char* extern_buffer = new char[BUFFER_SIZE]; memset(extern_buffer, 0, BUFFER_SIZE); const wchar_t* intern_from = src_wstr; const wchar_t* intern_from_end = intern_from + ws.size(); const wchar_t* intern_from_next = 0; char* extern_to = extern_buffer; char* extern_to_end = extern_to + BUFFER_SIZE; char* extern_to_next = 0; typedef std::codecvt<wchar_t, char, mbstate_t> CodecvtFacet; CodecvtFacet::result cvt_rst = std::use_facet<CodecvtFacet>(sys_loc).out( out_cvt_state, intern_from, intern_from_end, intern_from_next, extern_to, extern_to_end, extern_to_next); if (cvt_rst != CodecvtFacet::ok) { switch(cvt_rst) { case CodecvtFacet::partial: std::cerr << "partial"; break; case CodecvtFacet::error: std::cerr << "error"; break; case CodecvtFacet::noconv: std::cerr << "noconv"; break; default: std::cerr << "unknown"; } std::cerr << ", please check out_cvt_state." << std::endl; } std::string result = extern_buffer; delete []extern_buffer; return result; }
最後補充說明一下std::use_facet<CodecvtFacet>(sys_loc).in()和 std::use_facet<CodecvtFacet>(sys_loc).out()。sys_loc是系統的locale,這個 locale中就包含着特定的codecvt facet,咱們已經typedef爲了CodecvtFacet。用use_facet對CodecvtFacet進行了實例化,因此可使用這個 facet的方法in()和out()。安全
五、利用fstream轉換
C++的流和本地化策略集
BS在設計C++流的時候但願其具有智能化,而且是可擴展的智能化,也就是說,C++的流能夠「讀懂」一些內容。好比:
框架
std::cout << 123 << "ok" << std::endl;
這句代碼中,std::cout是能判斷出123是int而"ok"是const char[3]。利用流的智能,甚至能夠作一些基礎類型的轉換,好比從int到string,string到int:
函數
std::string str("123"); std::stringstream sstr(str); int i; sstr >> i; int i = 123; std::stringstream sstr; sstr << i; std::string str = sstr.str();
儘管如此,C++並不知足,C++甚至但願流能「明白」時間,貨幣的表示法。而時間和貨幣的表示方法在世界範圍內是不一樣的,因此,每個流都有本身的 locale在影響其行爲,C++中叫作激活(imbue,也有翻譯成浸染)。而咱們知道,每個locale都有多個facet,這些facet並不是老是被use_facet使用的。決定使用哪些facet的,是流的緩存basic_streambuf及其派生類basic_stringbuf和 basic_filebuf。咱們要用到的facet是codecvt,這個facet只被basic_filebuf使用——這就是爲何只能用 fstream來實現寬窄轉換,而沒法使用sstream來實現的緣由。
頭文件:編碼
//filename string_wstring_fstream.hpp #ifndef STRING_WSTRING_FSTREAM_HPP #define STRING_WSTRING_FSTREAM_HPP #include <string> const std::wstring s2ws(const std::string& s); const std::string ws2s(const std::wstring& s); #endif
實現:spa
#include <string> #include <fstream> #include "string_wstring_fstream.hpp" const std::wstring s2ws(const std::string& s) { std::locale sys_loc(""); std::ofstream ofs("cvt_buf"); ofs << s; ofs.close(); std::wifstream wifs("cvt_buf"); wifs.imbue(sys_loc); std::wstring wstr; wifs >> wstr; wifs.close(); return wstr; } const std::string ws2s(const std::wstring& s) { std::locale sys_loc(""); std::wofstream wofs("cvt_buf"); wofs.imbue(sys_loc); wofs << s; wofs.close(); std::ifstream ifs("cvt_buf"); std::string str; ifs >> str; ifs.close(); return str; }
在窄到寬的轉化中,咱們先使用默認的本地化策略集(locale)將s經過窄文件流ofs傳入文件,這是char到char的傳遞,沒有任何轉換;而後咱們打開寬文件流wifs,並用系統的本地化策略集(locale)去激活(imbue)之,流在讀回寬串wstr的時候,就是char到wchar_t的轉換,而且由於激活了sys_loc,因此實現標準窄到寬的轉換。翻譯
在寬到窄的轉化中,咱們先打開的是寬文件流wofs,而且用系統的本地化策略集 sys_loc激活(imbue)之,這時候,由於要寫的文件cvt_buf是一個外部編碼,因此執行了從wchar_t到char的標準轉換。讀回來的文件流從char到char,不作任何轉換。
六、國際化策略
硬編碼的硬傷
咱們如今知道,C/C++的寬窄轉換是依賴系統的locale的,而且在運行時完成。考慮這樣一種狀況,咱們在簡體中文Windows下編譯以下語句:
const char* s = "中文abc";
根據咱們以前的討論,編譯器將按照Windows Codepage936(GB2312)對這個字符串進行編碼。若是咱們在程序中運行寬窄轉換函數,將s轉換爲寬字符串ws,若是這個程序運行在簡體中文環境下是沒問題的,將執行從GB2312到UCS-2BE的轉換;可是,若是在其餘語言環境下,好比是繁體中文BIG5,程序將根據系統的locale執行從BIG5到UCS-2BE的轉換,這顯然就出現了錯誤。
補救
有沒有補救這個問題的辦法呢?一個解決方案就是執行不依賴locale的寬窄轉換。實際上,這就已經不是寬窄轉換之間的問題了,而是編碼之間轉換的問題了。咱們能夠用GNU的libiconv實現任意編碼間的轉換,對於以上的具體狀況,指明是從GB2312到UCS-2BE就不會出錯。(請參考本人前面的章節:win32下的libiconv),但這顯然是一個笨拙的策略:咱們在簡體中文Windows下必須使用GB2312到UCS-2BE版本的寬窄轉換函數;到了BIG5環境下,就必須從新寫從BIG5到UCS-2BE的寬窄轉換函數。
Windows的策略
Windows的策略是淘汰了窄字符串,乾脆只用寬字符串。全部的硬編碼所有加上特定宏,好比TEXT(),若是程序是所謂Unicode編譯,在編譯時就翻譯爲UCS2-BE——Windows自稱爲Unicode編程,其本質是使用了UCS-2BE的16位寬字符串。
Linux的策略
Linux下根本就不存在這個問題!由於各類語言的Linux都使用UTF-8的編碼,因此,不管系統locale如何變化,窄到寬轉換的規則一直是UTF-8到UTF32-BE 。
跨平臺策略
由於在16位的範圍內,UTF32-BE的前16位爲0,後16位與UCS2-BE是同樣的,因此,即便wchar_t的sizeof()不同,在通常狀況下,跨平臺使用寬字符(串)也應該是兼容的。可是依然存在潛在的問題,就是那些4字節的UTF32編碼。
gettext策略
以上都是將ASCII及之外的編碼硬編碼在程序中的辦法。GNU的gettext提供了另一種選擇:在程序中只硬編碼ASCII,多語言支持由gettext函數庫在運行時加載。(對gettext的介紹請參考本人前面的章節:Win32下的GetText)。 gettext的多語言翻譯文件不在程序中,而是單獨的提出來放在特定的位置。gettext明確的知道這些翻譯文件的編碼,因此能夠準確的告訴給系統翻譯的正確信息,而系統將這些信息以當前的系統locale編碼成窄字符串反饋給程序。例如,在簡體中文Windows中,gettext的po文件也能夠以UTF-8儲存,gettext將po文件翻譯成mo文件,確保mo文件在任何系統和語言環境下都可以正確翻譯。在運行是傳給win32程序的窄串符合當前locale,是GB2312。gettext讓國際化的翻譯更加的方便,缺點是目前我沒找到支持寬字符串的版本(聽說是有ugettext()支持寬字符串),因此要使用gettext只能使用窄字符串。可是gettext能夠轉換到寬字符串,並且不會出現寬窄轉換的問題,由於gettext是運行時根據locale翻譯的。例如:
const char* s = gettext("Chinese a b c");
其中"Chinese a b c"在po中的翻譯是"中文abc"
使用依賴locale的運行時寬窄轉換函數:
const std::wstring wstr = s2ws(s);
運行時調用該po文件對應的mo文件,在簡體中文環境下就以GB2312傳給程序,在繁體中文中就以BIG5傳給程序,這樣s2ws()總可以正常換算編碼。
更多
在本文的最後,我想回到C++的stream問題上。用fstream轉換如此的簡單,sstream卻不支持。改造一個支持codecvt的string stream須要改造basic_stringbuf。basic_stringbuf和basic_filebuf都派生自 basic_streambuf,所不一樣的是basic_filebuf在構造和open()的時候調用了codecvt,只須要在 basic_stringbuf中添加這個功能就能夠了。提及來容易,其實是須要從新改造一個STL模板,儘管這些模板源代碼都是在標準庫頭文件中現成的,可是我仍是水平有限,沒有去深究了。另一個思路是構建一個基於內存映射的虛擬文件,這個框架在boost的iostreams庫中,有興趣的朋友能夠深刻的研究。
(完)