被Unicode、Boost、C++和codecvts难倒了

16

在C++中,我想使用Unicode来进行操作。但是在深入了解Unicode后,我却遇到了一系列的困惑、头痛和本地化问题。

在Boost中,我尝试着使用Unicode文件路径,并且希望能够使用带有Unicode输入的Boost程序选项库。我已经阅读了关于本地化、编码转换、Unicode编码以及Boost的相关资料。

我目前的尝试是使用一个codecvt将UTF-8字符串转换为平台的编码(在POSIX上是UTF-8,在Windows上是UTF-16),并且尽量避免使用wchar_t

实际上,我最接近成功的尝试是使用Boost.Locale,在输出时将UTF-8字符串转换为UTF-32字符串。

#include <string>
#include <boost/locale.hpp>
#include <locale>

int main(void)
{
  std::string data("Testing, 㤹");

  std::locale fromLoc = boost::locale::generator().generate("en_US.UTF-8");
  std::locale toLoc   = boost::locale::generator().generate("en_US.UTF-32");

  typedef std::codecvt<wchar_t, char, mbstate_t> cvtType;
  cvtType const* toCvt = &std::use_facet<cvtType>(toLoc);

  std::locale convLoc = std::locale(fromLoc, toCvt);

  std::cout.imbue(convLoc);
  std::cout << data << std::endl;

  // Output is unconverted -- what?

  return 0;
}

我记得我曾经使用宽字符进行某种转换,但是我真的不知道自己在做什么。此时我也不知道适合此工作的正确工具是什么。请帮忙?

3个回答

12

好的,在长达几个月的时间里,我已经解决了它,并且我想帮助未来的人们。

首先,codecvt 的方法是错误的。Boost.Locale 在其 boost::locale::conv 命名空间中提供了一种简单的转换字符集的方式。以下是一个示例(还有其他基于区域设置不同的示例)。

#include <boost/locale.hpp>
namespace loc = boost::locale;

int main(void)
{
  loc::generator gen;
  std::locale blah = gen.generate("en_US.utf-32");

  std::string UTF8String = "Tésting!";
  // from_utf will also work with wide strings as it uses the character size
  // to detect the encoding.
  std::string converted = loc::conv::from_utf(UTF8String, blah);

  // Outputs a UTF-32 string.
  std::cout << converted << std::endl;

  return 0;
}

如您所见,如果将“en_US.utf-32”替换为“”,它会输出用户的语言环境。

我仍然不知道如何让std::cout始终这样做,但是Boost.Locale的translate()函数可以输出用户的语言环境。

至于跨平台使用UTF-8字符串的文件系统,似乎是可能的,这里有一个链接教你如何做到


1
这是一个链接,它不会跳转到索引页面(针对上一个链接)http://www.boost.org/doc/libs/1_51_0/libs/locale/doc/html/default_encoding_under_windows.html - elegant dice
你说使用codecvt是不好的,但为什么Boost在其文件系统中,特别是path类中使用codecvt作为转换方法呢? - hakunami
嗯...我在2011年回答过这个问题,所以我不太确定当时我的想法是什么。我想我发布的codecvt方式是错误的做法。Boost.Locale本身使用codecvts与Boost.Filesystem进行接口交互。 - Jookia

3
  std::cout.imbue(convLoc);
  std::cout << data << std::endl;

这不进行任何转换,因为它使用的是codecvt<char, char, mbstate_t>,这是一个无操作的操作。只有文件流使用codecvt。std::cout根本不需要执行任何转换。
要强制Boost.Filesystem在Windows上将窄字符串解释为UTF-8,请使用带有UTF-8 ↔ UTF-16 codecvt facet的区域设置使用boost::filesystem::imbue。Boost.Locale提供了后者的实现。

@Jookia:再次提醒,你的问题太过笼统:“我在一般情况下遇到了麻烦”!“我想在我的程序中使用UTF-8字符串”,请继续说明!这是我所做的。“将用户的区域设置从/转换为UTF-8以便与cout和cin一起使用”,为什么?只需假设它是UTF-8,并让那些使用旧编码的人将其编码更改为UTF-8。在Windows上,您应该使用wcin和wcout来读写Unicode数据,但这将是不可移植的,因为您将不得不维护两个版本的代码,一个在Windows上使用wcout,另一个在非Windows上使用cout。你不想要这个,对吧? - Yakov Galka
但是我想使用UTF-8和Boost,这似乎是不可能的。在Boost的某些部分中,虽然有些不方便但可以实现Unicode(例如Boost.Filesystem),但有些部分在Windows上根本不支持Unicode(例如Boost.Interprocess),有些则在跨平台代码中很麻烦(例如Boost.Program_Options)。 "它使用宽字串"不完全正确。Boost的一些部分使用窄字符(例如Boost.Interprocess),有些则两者都用(例如Boost.Filesystem)。问题在于,使用窄字符串的那些部分默认假定本地编码而不是UTF-8,并强加了负担。 - Yakov Galka
在 Boost 社区中,存在一场关于弃用宽字符和假设所有窄字符串为 UTF-8 的争论(链接:http://lists.boost.org/Archives/boost/2011/01/174850.php)。我们(UTF-8 的支持者)目前处于劣势,因为没有太多的需求,而大多数 Boost 开发人员(例如 Filesystem 的作者)生活在 Unix 世界中,并没有面临编写 Unicode 正确的生产代码并可在 Windows 和 Linux 之间移植的实际麻烦。如果你想改变现状,请再次在 Boost 邮件列表中开展讨论。 - Yakov Galka
@Jookia: 为什么放弃?是的,你不能总是使用现有的库进行便携式编程,需要编写样板代码来使用其他方式进行编程。在可能范围内支持它的选择取决于您。 - Yakov Galka
我放弃了,因为我并没有完全理解问题所在,也无法推断出可能的解决方案。 - Jookia
显示剩余3条评论

3
Boost文件系统iostream替换类在使用Visual C++时可以很好地处理UTF-16。然而,在Windows中使用g ++时,它们无法支持任意文件名,至少在Boost版本1.47中是如此。有一条代码注释解释了这个问题; 实质上,Visual C++标准库提供了非标准的基于wchar_t的构造函数,Boost文件系统类利用了这些扩展功能,但g++不支持这些扩展功能。
一种解决方法是使用8.3短文件名,但这种解决方案有些脆弱,因为在旧版Windows中,用户可以关闭自动生成短文件名的功能。
使用Boost文件系统在Windows中的示例代码:
#include "CmdLineArgs.h"        // CmdLineArgs
#include "throwx.h"             // throwX, hopefully
#include "string_conversions.h" // ansiOrFillerFrom( wstring )

#include <boost/filesystem/fstream.hpp>     // boost::filesystem::ifstream
#include <iostream>             // std::cout, std::cerr, std::endl
#include <stdexcept>            // std::runtime_error, std::exception
#include <string>               // std::string
#include <stdlib.h>             // EXIT_SUCCESS, EXIT_FAILURE
using namespace std;
namespace bfs = boost::filesystem;

inline string ansi( wstring const& ws ) { return ansiWithFillersFrom( ws ); }

int main()
{
    try
    {
        CmdLineArgs const   args;
        wstring const       programPath     = args.at( 0 );

        hopefully( args.nArgs() == 2 )
            || throwX( "Usage: " + ansi( programPath ) + " FILENAME" );

        wstring const       filePath        = args.at( 1 );
        bfs::ifstream       stream( filePath );     // Nice Boost ifstream subclass.
        hopefully( !stream.fail() )
            || throwX( "Failed to open file '" + ansi( filePath ) + "'" );

        string line;
        while( getline( stream, line ) )
        {
            cout << line << endl;
        }
        hopefully( stream.eof() )
            || throwX( "Failed to list contents of file '" + ansi( filePath ) + "'" );

        return EXIT_SUCCESS;
    }
    catch( exception const& x )
    {
        cerr << "!" << x.what() << endl;
    }
    return EXIT_FAILURE;
}

1
我正在尝试跨平台实现它。 - Jookia
1
@Jookia:好的。我假设你只限制在UTF-8本地*nix(和Mac)和Windows上。支持通用跨平台,我认为这不是一个人可以完成的。祝你好运! - Cheers and hth. - Alf
1
@Jookia:这个答案证明了我以下某些说法的正确性。在Windows上使用boost.filesystem与Unicode,必须使用wstring,在非Windows上则一定要使用string。这就是boost.filesystem不隐藏平台差异,也不使编写跨平台代码更简单的方式。我必须承认,在boost.fs的情况下,您可以更改其解释窄字符串为UTF-8的方式,从而使移植代码更容易。然而,问题在于boost只需更改boost.fs中的两行代码即可让我们的生活变得更轻松。遗憾的是他们不想这样做。 - Yakov Galka
@ybungalobill:请注意,在Windows中,boost文件系统不支持带有g++的一般文件名,并且该问题无法通过在所有地方使用UTF-8编码来解决。 - Cheers and hth. - Alf
@AlfP.Steinbach说的“boost filesystem does not support general filenames”可能意味着,例如,在所有其他可能的解释中,“boost::filesystem::remove()不支持一般文件名”。现在清楚了。无论如何,UTF-8的重点不是神奇地支持Unicode,而是提供一个统一的可移植接口,使具有此类支持的系统之间保持一致。 - Yakov Galka
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接