排查GCC10下path::wstring转中文出异常
#include <iostream> #include <filesystem> #include <locale> using namespace std; namespace fs = std::filesystem; int main() { std::locale::global(std::locale("zh_CN.utf8")); fs::path tt(u8"你好"); std::wcout << tt.wstring() << endl; return 0; }
以上测试代码通过以下编译命令编译
g++ tt.cpp -D_GLIBCXX_USE_CXX11_ABI=1 -std=c++17 -lstdc++fs -o tt
再运行,会出现以下错误
terminate called after throwing an instance of 'std::filesystem::__cxx11::filesystem_error' what(): filesystem error: Cannot convert character sequence: Invalid or incomplete multibyte or wide character Aborted
调试 path::wstring
的实现,可以看到问题主要在 /usr/include/c++/10/bits/fs_path.h
里的以下代码:(GitHub Code Viewer)
_WString __wstr(__a); struct _UCvt : std::codecvt<_CharT, char, std::mbstate_t> { } __cvt; if (__str_codecvt_in_all(__first, __last, __wstr, __cvt)) return __wstr;
这里的代码使用了默认的 codecvt
实现来做编码转换,我们已经设置了全局的 locale,这里为什么会无法正确转换?
看 codecvt
的定义,它有个 __c_locale
类型的成员变量(GitHub Code Viewer),说明内部记录了一个 locale,转换的时候需要用到 locale,无法正确转换,那一定是跟全局的 locale 不一样。那这个成员变量又是怎么创建的?它被设置成了什么?
查看实现,可以看到以下代码(GitHub Code Viewer),它是来自于 _S_get_c_locale
。
codecvt<char, char, mbstate_t>:: codecvt(size_t __refs) : __codecvt_abstract_base<char, char, mbstate_t>(__refs), _M_c_locale_codecvt(_S_get_c_locale()) { }
再看 _S_get_c_locale
的实现(GitHub Code Viewer)
__c_locale locale::facet::_S_c_locale; const char locale::facet::_S_c_name[2] = "C"; #ifdef __GTHREADS __gthread_once_t locale::facet::_S_once = __GTHREAD_ONCE_INIT; #endif void locale::facet::_S_initialize_once() { // Initialize the underlying locale model. _S_create_c_locale(_S_c_locale, _S_c_name); } __c_locale locale::facet::_S_get_c_locale() { #ifdef __GTHREADS if (__gthread_active_p()) __gthread_once(&_S_once, _S_initialize_once); else #endif { if (!_S_c_locale) _S_initialize_once(); } return _S_c_locale; }
可以看到,这里默认 locale 为 C,而不是空字符串。这样它就不可能是全局 locale。
由此看到,不改标准库是不可能解决这个问题的。但这个问题在 GCC 7.5.0 里是不存在的。
看 GCC 7.5.0 里 path 编码转换有关的代码(GitHub Code Viewer)
codecvt_utf8<_CharT> __cvt; basic_string<_CharT, _Traits, _Allocator> __wstr{__a}; if (__str_codecvt_in(__first, __last, __wstr, __cvt)) return __wstr;
可以看到这里用的是 codecvt_utf8
,并不是默认的实现,所以没问题。因此,要让 GCC 10 没问题,可以按 7.5.0 的实现来改。
但改标准库不是件好的行为,应该找到 GCC 后面的版本为什么没有使用 7.5.0 的实现的原因。原因看《[PATCH] PR libstdc++/90281 Fix string conversions for filesystem::path》,看上去主要是为了解决 Windows 下的编码转换问题,没看出来为啥要把 Linux 下的也改了。