C++的字符串轉換函數mbstowcs使用時容易產生bug。。。
rapidxml_utils.hpp 的file(const char*filename)函數內會異常宕機。。。
需要在函數最開始添加
locale::global(locale(""));
from http://blog.sina.com.cn/s/blog_55c1b83b0100wbah.html
1 問題
在 Windows XP 多語言簡中環境下,用 VC2005 中的 std::fstream 打開中文名文件,系統報錯找不到此文件。
std::ifstream file("\xd6\xd0.txt"); // GBK 編碼的 "中.txt" if (!file) { std::cerr <<"Cannot open file!"; // Oops! } |
2 原因
在 VC2005 中 std::fstream 的打開文件的函數實現里,傳入的 char const* 文件名作為多字節首先被mbstowcs 轉換成寬字節后,再轉發給 Unicode 版本的 API 進行實際的打開文件操作。見 fiopen.cpp:
_MRTIMP2_NCEEPURE FILE *__CLRCALL_PURE_OR_CDECL _Fiopen(const char *filename, ios_base::openmode mode, int prot) { // open wide-named file with byte name wchar_twc_name[FILENAME_MAX]; if (mbstowcs_s(NULL, wc_name, FILENAME_MAX, filename,FILENAME_MAX - 1) != 0) return (0); return _Fiopen(wc_name, mode, prot); } |
問題的關鍵在於,對於 mbstowcs 函數來說,它需要知道多字節的編碼類型才能正確的將其轉換成寬字節的 unicode,很可惜這個編碼類型並沒有體現在函數的參數列表里,而是隱含依賴全局的 locale 。更加不幸的是,全局 locale 默認沒有使用系統當前語言,而是設置為沒什么用處的 "C" locale 。於是 GBK 編碼的文件名在 "C" locale 下轉換錯誤,悲劇發生了……
3 解
知道了原因,解就很簡單了。在調用 mbstowcs 或使用它的函數之前,先用 setlocale 將全局默認 locale 設為當前系統默認 locale :
setlocale(LC_ALL, ""); |
如果是在非中文系統上轉 GBK 編碼,就需要指定中文 locale :
setlocale(LC_ALL, "chs"); // chs 是 VC 里簡中的 locale 名字 |
還有一種方法,直接使用寬字節版本的API,之前的編碼由自己轉換好,避免系統語言環境設置的影響。在 VS2005 中 fstream 有個擴展,可以直接打開寬字節文件名:
std::ifstream file(L"\u4E2D.txt"); // UCS2 編碼的“中.txt” |
4 引申
API 中隱藏依賴關系是不好的,這種隱藏總意謂着外部環境能通過潛規則來影響 API 的功能。這影響了該API的復用性,可測性,也容易讓用戶出現意外錯誤。進一步設想一下,如果環境原來的 locale 是被其它代碼塊故意設置的,如果為了修正打開中文名文件的 Bug 而冒冒然修改當前全局的 locale ,很可能會讓依賴於原 locale 工作的代碼出現 bug 。在這樣的 API 設計下,如果要盡量避免顧此失彼的發生,我們可以在修改前保存當前的 locale ,用完后再恢復回原來的 locale 。在 C++ 里,最好是將這樣的邏輯用 RAII 來封裝:
class scoped_locale { public: scoped_locale(std::string const& loc_name) :_new_locale(loc_name) , _setted(false) { try { char const* old_locale =setlocale(LC_CTYPE, _new_locale.c_str()); if (NULL != old_locale) { _old_locale =old_locale; _setted = true; } } catch (...) { } } ~scoped_locale() { try { if(_setted) { char const* pre_locale = setlocale(LC_CTYPE, _old_locale.c_str()); if(pre_locale) { assert(pre_locale == _new_locale); _setted = false; } } } catch (...){ } } private: std::string _new_locale; std::string _old_locale; bool _setted; }; |
原代碼可以改為:
{ scoped_locale change_locale_to(""); std::ifstream file("\xd6\xd0.txt"); // GBK 編碼的“中.txt” if (!file) { std::cerr << "Cannot open file!"; // Oops! } } |
當然,如果是多線程環境的話,還需要查明 locale 的全局性是進程級的還是線程級的。如果是前者,那還是會有潛在的相互影響的風險。從這點上來看,C/C++ 標准庫中 mbstowcs 的設計是有瑕疵的。這也從反面體現了 Dependency Injection 思想的重要性。在 Win32 API 有個類似的函數 WideCharToMultiByte() ,它的作用也是進行多字節到寬字節的編碼轉換,但在API設計上,它就將 code page 作為第一個入參顯示傳入,而不是默認使用全局系統的某個狀態。用它來寫一個通用的轉換函數就可以避免 mbstowcs 的問題了:
std::wstring native_to_utf16(std::string const& native_string) { UINT const codepage= CP_ACP; DWORD const sizeNeeded = MultiByteToWideChar( codepage, 0, native_string.c_str(), -1, NULL, 0); std::vector<wchar_t> buffer(sizeNeeded, 0); if (0 == MultiByteToWideChar(codepage, 0, native_string.c_str(), -1, &buffer[0], buffer.size())) { throw std::runtime_error("wrong convertion from native string to utf16"); } return std::wstring(buffer.begin(), buffer.end()); } |
