WChars,编码,标准和可移植性

时间:2011-06-10 00:35:41

标签: c++ unicode wchar-t

以下可能不符合SO问题;如果它超出范围,请随时告诉我离开。这里的问题基本上是,“我是否正确理解C标准,这是正确的方法吗?”

我想请求澄清,确认和更正我对C中字符处理的理解(以及C ++和C ++ 0x)。首先,一个重要的观察:

可移植性和序列化是正交概念。

便携式内容包括C,unsigned intwchar_t。可序列化的东西是uint32_t或UTF-8。 “可移植”意味着您可以重新编译相同的源并在每个支持的平台上获得工作结果,但二进制表示可能完全不同(或甚至不存在,例如TCP-over-carrier pigeon)。另一方面,可序列化的东西总是具有相同的表示,例如我可以在Windows桌面,手机或牙刷上阅读的PNG文件。便携式东西是内部的,可序列化的东西处理I / O.便携式东西是类型安全的,可序列化的东西需要打字。 < /前导>

在C中进行字符处理时,有两组分别与可移植性和序列化相关:

  • wchar_tsetlocale()mbsrtowcs() / wcsrtombs() C标准对“编码”一无所知;事实上,它与任何文本或编码属性完全无关。它只是说“你的入口点是main(int, char**);你得到的类型wchar_t可以容纳你所有系统的字符;你可以获得读取输入字符序列的函数,并使它们成为可行的字符串,反之亦然。

  • iconv()和UTF-8,16,32:在定义明确的固定编码之间进行转码的函数/库。由iconv处理的所有编码都得到普遍理解和同意,但有一个例外。

C的可移植,与编码无关的世界与wchar_t可移植字符类型和确定性外部世界之间的桥梁是WCHAR-T和UTF 之间的 iconv转换。

那么,我是否应该始终将我的字符串内部存储在与编码无关的wstring中,通过wcsrtombs()与CRT接口,并使用iconv()进行序列化?从概念上讲:

                        my program
    <-- wcstombs ---  /==============\   --- iconv(UTF8, WCHAR_T) -->
CRT                   |   wchar_t[]  |                                <Disk>
    --- mbstowcs -->  \==============/   <-- iconv(WCHAR_T, UTF8) ---
                            |
                            +-- iconv(WCHAR_T, UCS-4) --+
                                                        |
       ... <--- (adv. Unicode malarkey) ----- libicu ---+

实际上,这意味着我会为我的程序入口点编写两个样板包装器,例如对于C ++:

// Portable wmain()-wrapper
#include <clocale>
#include <cwchar>
#include <string>
#include <vector>

std::vector<std::wstring> parse(int argc, char * argv[]); // use mbsrtowcs etc

int wmain(const std::vector<std::wstring> args); // user starts here

#if defined(_WIN32) || defined(WIN32)
#include <windows.h>
extern "C" int main()
{
  setlocale(LC_CTYPE, "");
  int argc;
  wchar_t * const * const argv = CommandLineToArgvW(GetCommandLineW(), &argc);
  return wmain(std::vector<std::wstring>(argv, argv + argc));
}
#else
extern "C" int main(int argc, char * argv[])
{
  setlocale(LC_CTYPE, "");
  return wmain(parse(argc, argv));
}
#endif
// Serialization utilities

#include <iconv.h>

typedef std::basic_string<uint16_t> U16String;
typedef std::basic_string<uint32_t> U32String;

U16String toUTF16(std::wstring s);
U32String toUTF32(std::wstring s);

/* ... */

这是使用纯标准C / C ++编写一个惯用的,可移植的,通用的,编码无关的程序核心的正确方法,以及使用iconv明确定义的UTF I / O接口吗? (请注意,Unicode规范化或变音符替换等问题超出了范围;只有在您确定实际需要 Unicode (与您可能想要的任何其他编码系统相对)之后才能处理这些问题。细节,例如使用像libicu这样的专用库。)

更新

经过许多非常好的评论后,我想补充几点意见:

  • 如果您的应用程序明确要处理Unicode文本,您应该将iconv转换为核心部分,并在内部使用uint32_t / char32_t - 字符串与UCS -4。

  • Windows:虽然使用宽字符串通常很好,但似乎与控制台(任何控制台)的交互都是有限的,因为似乎不支持任何合理的多字节控制台编码并且mbstowcs基本上没用(除了琐碎的扩大)。从Explorer-drop和GetCommandLineW + CommandLineToArgvW一起接收宽字符串参数(也许应该有一个单独的Windows包装器)。

  • 文件系统:文件系统似乎没有任何编码概念,只是将任何以null结尾的字符串作为文件名。大多数系统采用字节字符串,但Windows / NTFS采用16位字符串。在发现存在哪些文件以及处理该数据时(例如,不构成有效UTF16的char16_t序列(例如裸代理)是有效的NTFS文件名),您必须小心。标准C fopen无法打开所有NTFS文件,因为没有可能的转换将映射到所有可能的16位字符串。可能需要使用特定于Windows的_wfopen。作为必然结果,通常没有明确定义的“多少个字符”包含给定文件名的概念,因为首先没有“字符”的概念。注意事项。

4 个答案:

答案 0 :(得分:21)

  

这是使用纯标准C / C ++编写惯用,可移植,通用,编码无关的程序核心的正确方法

不,并且完全没有办法满足所有这些属性,至少如果您希望程序在Windows上运行。在Windows上,您必须几乎无处不在地忽略C和C ++标准,并且只使用wchar_t(不一定是在内部,而是在系统的所有接口上)。例如,如果您从

开始
int main(int argc, char** argv)

您已经失去了对命令行参数的Unicode支持。你必须写

int wmain(int argc, wchar_t** argv)

代替,或使用GetCommandLineW函数,其中没有一个在C标准中指定。

更具体地说,

  • Windows上任何支持Unicode的程序都必须主动忽略C和C ++标准,例如命令行参数,文件和控制台I / O,或文件和目录操作。这肯定不是惯用的。请改用Microsoft扩展或包装器,如Boost.Filesystem或Qt。
  • 可移植性非常难以实现,尤其是对于Unicode支持。你必须做好准备,认为你认识的一切都可能是错的。例如,您必须考虑用于打开文件的文件名可能与实际使用的文件名不同,并且两个看似不同的文件名可能代表同一个文件。创建两个文件 a b 之后,最终可能会有一个文件 c ,或者两个文件 d e ,其文件名与传递给操作系统的文件名不同。您需要一个外部包装器库或许多#ifdef s。
  • 编码不可知通常只是在实践中不起作用,特别是如果你想要便携的话。您必须知道wchar_t是Windows上的UTF-16代码单元,char通常(并非总是)是Linux上的UTF-8代码单元。编码感知通常是更理想的目标:确保您始终知道您使用的编码,或使用抽象它们的包装库。

我认为我必须得出结论,除非您愿意使用额外的库和特定于系统的扩展,并且在其中投入大量精力,否则完全不可能在C或C ++中构建可移植的Unicode应用程序。不幸的是,大多数应用程序已经在相对简单的任务中失败,例如“将希腊字符写入控制台”或“以正确的方式支持系统允许的任何文件名”,而这些任务只是实现真正的Unicode支持的第一个微小步骤。 / p>

答案 1 :(得分:9)

我会避免wchar_t类型,因为它依赖于平台(不是你的定义“可序列化”):Windows上的UTF-16和大多数类Unix系统上的UTF-32。而是使用C ++ 0x / C1x中的char16_t和/或char32_t类型。 (如果您没有新的编译器,请暂时将其设置为uint16_tuint32_t。)

DO 定义在UTF-8,UTF-16和UTF-32功能之间进行转换的功能。

不要编写每个字符串函数的重载窄/宽版本,就像Windows API使用-A和-W一样。选择一个首选编码以在内部使用,并坚持下去。对于需要不同编码的内容,请根据需要进行转换。

答案 2 :(得分:8)

wchar_t的问题是与编码无关的文本处理过于困难,应该避免。如果你坚持使用“纯C”,你可以使用所有w*函数,如wcscat和朋友,但如果你想做更复杂的事情,那么你必须潜入深渊

如果您选择其中一种UTF编码,wchar_t的某些内容要比它们更难:

  • 解析Javascript:标识符可以包含BMP之外的某些字符(并假设您关心这种正确性)。

  • HTML:如何将&#65536;转换为wchar_t的字符串?

  • 文本编辑器:如何在wchar_t字符串中找到字形集群边界?

如果我知道字符串的编码,我可以直接检查字符。如果我不知道编码,我不得不希望无论我想用字符串做什么都是由某个库函数实现的。因此wchar_t的可移植性有点无关紧要,因为我认为它不是特别有用的数据类型。

您的计划要求可能会有所不同,wchar_t可能适合您。

答案 3 :(得分:6)

鉴于iconv不是“纯标准C / C ++”,我认为您不满足自己的规范。

codecvtchar32_t会有新的char16_t方面,所以只要您保持一致并且选择一个字符类型+编码,我就不会看到您的错误。方面就在这里。

小平面在22.5 [locale.stdcvt](来自n​​3242)中描述。


我不明白这至少不能满足你的一些要求:

namespace ns {

typedef char32_t char_t;
using std::u32string;

// or use user-defined literal
#define LIT u32

// Communicate with interface0, which wants utf-8

// This type doesn't need to be public at all; I just refactored it.
typedef std::wstring_convert<std::codecvt_utf8<char_T>, char_T> converter0;

inline std::string
to_interface0(string const& s)
{
    return converter0().to_bytes(s);
}

inline string
from_interface0(std::string const& s)
{
    return converter0().from_bytes(s);
}

// Communitate with interface1, which wants utf-16

// Doesn't have to be public either
typedef std::wstring_convert<std::codecvt_utf16<char_T>, char_T> converter1;

inline std::wstring
to_interface0(string const& s)
{
    return converter1().to_bytes(s);
}

inline string
from_interface0(std::wstring const& s)
{
    return converter1().from_bytes(s);
}

} // ns

然后您的代码可以使用ns::stringns::char_tLIT'A'&amp; LIT"Hello, World!"肆无忌惮地放弃,不知道底层代表是什么。然后在需要时使用from_interfaceX(some_string)。它也不会影响全局语言环境或流。帮助者可以根据需要聪明,例如, codecvt_utf8可以处理'标题',我认为它是从BOM(ditto codecvt_utf16)等棘手的东西中获得Standardese。

事实上我上面写的是尽可能短,但你真的想要这样的帮手:

template<typename... T>
inline ns::string
ns::from_interface0(T&&... t)
{
    return converter0().from_bytes(std::forward<T>(t)...);
}

可让您访问每个[from|to]_bytes成员的3次重载,接受例如const char*或范围。