#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 下的也改了。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注