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

发表回复

您的电子邮箱地址不会被公开。