在字符串、u16字符串和u32字符串之间进行转换

47

我一直在寻找一种方法来转换不同的Unicode字符串类型,并发现了这个方法。我不仅完全不理解这个方法(没有注释),而且文章暗示未来会有更好的方法。

如果这是最好的方法,请指出它的优点,如果不是,我想听听更好的建议。


1
请参见https://dev59.com/TXVC5IYBdhLWcg3w51ry。 - Mark Ransom
3个回答

96

mbstowcs()wcstombs()并不一定转换为UTF-16或UTF-32,它们会转换为wchar_t以及本地wchar_t编码。所有Windows区域设置都使用了一个两字节的wchar_t和UTF-16作为编码,但其他主要平台使用了一个4字节的wchar_t与UTF-32(甚至某些区域设置使用非Unicode编码)。一个只支持单字节编码的平台甚至可以有一个一字节的wchar_t,并且编码会因区域设置而异。因此,对于可移植性和Unicode来说,wchar_t似乎是一个糟糕的选择。*

C++11引入了一些更好的选项:新的std::codecvt特化、新的codecvt类以及一个新的模板,使得使用它们进行转换非常方便。

首先,用于使用codecvt的新模板类是std::wstring_convert。一旦创建了std::wstring_convert类的实例,就可以轻松地在字符串之间进行转换:

std::wstring_convert<...> convert; // ... filled in with a codecvt to do UTF-8 <-> UTF-16
std::string utf8_string = u8"This string has UTF-8 content";
std::u16string utf16_string = convert.from_bytes(utf8_string);
std::string another_utf8_string = convert.to_bytes(utf16_string);

为了进行不同的转换,您只需要不同的模板参数之一是codecvt facet。以下是一些易于与wstring_convert一起使用的新facet:
std::codecvt_utf8_utf16<char16_t> // converts between UTF-8 <-> UTF-16
std::codecvt_utf8<char32_t> // converts between UTF-8 <-> UTF-32
std::codecvt_utf8<char16_t> // converts between UTF-8 <-> UCS-2 (warning, not UTF-16! Don't bother using this one)

以下是使用它们的示例:
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>,char16_t> convert;
std::string a = convert.to_bytes(u"This string has UTF-16 content");
std::u16string b = convert.from_bytes(u8"blah blah blah");

新的std::codecvt特化有一个受保护的析构函数,因此使用起来比较困难。为了解决这个问题,您可以定义一个具有析构函数的子类,或者使用std::use_facet模板函数获取现有的codecvt实例。此外,这些特化存在一个问题,即无法在Visual Studio 2010中使用,因为typedef'd类型不支持模板特化,而该编译器将char16_t和char32_t定义为typedef。以下是定义自己的codecvt子类的示例:
template <class internT, class externT, class stateT>
struct codecvt : std::codecvt<internT,externT,stateT>
{ ~codecvt(){} };

std::wstring_convert<codecvt<char16_t,char,std::mbstate_t>,char16_t> convert16;
std::wstring_convert<codecvt<char32_t,char,std::mbstate_t>,char32_t> convert32;

char16_t专用于UTF-16和UTF-8之间的转换。char32_t专用于UTF-32和UTF-8之间的转换。

请注意,C++11提供的这些新转换方式并没有直接在UTF-32和UTF-16之间进行转换的方法。相反,您只需组合两个std::wstring_convert的实例即可。


***** 我想添加一条关于wchar_t及其目的的注释,以强调为什么它通常不应用于Unicode或便携式国际化代码。以下是我回答的简短版本 https://dev59.com/02gu5IYBdhLWcg3wv5ca#11107667

wchar_t是什么?

wchar_t被定义为可以将任何语言环境的字符编码转换为wchar_t,其中每个wchar_t表示一个代码点:

类型wchar_t是一个独特的类型,其值可以表示所支持语言环境中最大扩展字符集的所有成员的不同代码。 -- [basic.fundamental] 3.9.1/5

这并不要求wchar_t足够大以同时表示来自所有语言环境的任何字符。也就是说,wchar_t的编码可能因语言环境而异。这意味着您不能使用一个语言环境将字符串转换为wchar_t,然后使用另一个语言环境将其转换回char。

由于这似乎是wchar_t在实践中的主要用途,因此您可能会想知道它的用途是什么。

wchar_t的最初意图和目的是通过定义它,使文本处理变得简单,使其需要从字符串的代码单元到文本的字符的一对一映射,从而允许使用与ascii字符串相同的简单算法来处理其他语言。

不幸的是,wchar_t的要求假定字符和代码点之间存在一对一的映射以实现这一点。Unicode打破了这个假设,因此即使对于简单的文本算法,您也不能安全地使用wchar_t。

这意味着便携式软件既不能使用wchar_t作为不同语言环境之间的通用文本表示,也不能使用它来启用简单的文本算法。

wchar_t今天有什么用处?

对于便携式代码来说,没有太多用处。如果定义了__STDC_ISO_10646__,则wchar_t的值直接表示具有相同值的Unicode代码点,在所有语言环境中都是如此。这使得安全进行前面提到的语言环境之间的转换成为可能。但是,您不能仅依靠它来决定可以以这种方式使用wchar_t,因为尽管大多数Unix平台都定义了它,但Windows却没有,即使Windows在所有语言环境中使用相同的wchar_t语言环境。

我认为Windows不定义__STDC_ISO_10646__的原因是因为Windows使用UTF-16作为其wchar_t编码,并且因为UTF-16使用代理对来表示大于U+FFFF的代码点,这意味着UTF-16不满足__STDC_ISO_10646__的要求。

对于特定平台的代码,wchar_t可能更有用。在Windows上它是必需的(例如,某些文件如果不使用wchar_t文件名将无法打开),不过据我所知,Windows是唯一需要这样做的平台(因此我们可以将wchar_t视为“Windows_char_t”)。

回想起来,wchar_t显然不适用于简化文本处理,也不适用于存储与语言环境无关的文本。可移植的代码不应尝试将其用于这些目的。


非常感谢您提供如此深入的回答,这正是我所需要的。我可以确认一下,UTF-16转换为UTF-32需要先将其转换为UTF-8,然后再转换为UTF-32吗? - DrYap
2
顺便提一下,这个东西已经在libc++中实现了(还不是clang的标准c++库),以及VS2010中也有(除了我注意到的异常情况)。 - bames53
4
@towi看起来在gcc中仍未实现。只有MSVC和libc++支持该功能。 - bames53
wstring_convert和相关函数在C++17中已被弃用。 - unexpectedvalue
他们有一个我认为是不好的理由,详情请见我的观点 - bames53
显示剩余5条评论

15

我已经编写了一些助手函数来转换 UTF8 字符串(C++11):

#include <string>
#include <locale>
#include <codecvt>

using namespace std;

template <typename T>
string toUTF8(const basic_string<T, char_traits<T>, allocator<T>>& source)
{
    string result;

    wstring_convert<codecvt_utf8_utf16<T>, T> convertor;
    result = convertor.to_bytes(source);

    return result;
}

template <typename T>
void fromUTF8(const string& source, basic_string<T, char_traits<T>, allocator<T>>& result)
{
    wstring_convert<codecvt_utf8_utf16<T>, T> convertor;
    result = convertor.from_bytes(source);
}

使用示例:

// Unicode <-> UTF8
{
    wstring uStr = L"Unicode string";
    string str = toUTF8(uStr);

    wstring after;
    fromUTF8(str, after);
    assert(uStr == after);
}

// UTF16 <-> UTF8
{
    u16string uStr;
    uStr.push_back('A');
    string str = toUTF8(uStr);

    u16string after;
    fromUTF8(str, after);
    assert(uStr == after);
}

-2
据我所知,C++没有提供从或转换为UTF-32的标准方法。但是,对于UTF-16,有方法mbstowcs(多字节到宽字符字符串)和其反向方法wcstombs
如果您还需要UTF-32,则需要使用iconv,它在POSIX 2001中,但不在标准C中,因此在Windows上,您需要像libiconv这样的替代品。
以下是如何使用mbstowcs的示例:
#include <string>
#include <iostream>
#include <stdlib.h>

using namespace std;

wstring widestring(const string &text);

int main()
{
  string text;
  cout << "Enter something: ";
  cin >> text;

  wcout << L"You entered " << widestring(text) << ".\n";
  return 0;
}

wstring widestring(const string &text)
{
  wstring result;
  result.resize(text.length());
  mbstowcs(&result[0], &text[0], text.length());
  return result;
}

反过来就是这样:

string mbstring(const wstring &text)
{
  string result;
  result.resize(text.length());
  wcstombs(&result[0], &text[0], text.length());
  return result;
}

小挑剔:是的,我知道,wchar_t的大小是由实现定义的,所以它可能是4个字节(UTF-32)。然而,我不知道有哪个编译器会这样做。


7
Linux 上的 GCC 使用 UTF-32 来表示 wchar_t - dan04
7
据我所知,Windows 是唯一一个在 wstring 中使用 UTF-16 的常见平台。 - Head Geek
1
可能不算“常见”,但我认为AIX使用2字节的wchar_t和UTF-16。 - bames53
反转函数的问题在于,您可能需要一个比原始字符串中字符数更多元素的缓冲区。例如,如果您将带有日语的宽字符串转换为S-JIS,则文本将被截断。如果您将第一个参数设为NULL调用wcstombs,则该函数将返回存储原始字符串中所有字符所需的缓冲区大小。此外,在C++11之前,不能保证std::string中的元素连续存储,并且从C++11开始,有std::codecvt使整个过程变得轻松愉快。 - dreamlax

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