完全解密C++寬字符(二)

完全解密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庫中,有興趣的朋友能夠深刻的研究。
(完)

相關文章
相關標籤/搜索