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

Posted

技术标签:

【中文标题】WChars、编码、标准和可移植性【英文标题】:WChars, Encodings, Standards and Portability 【发布时间】:2011-09-12 03:35:08 【问题描述】:

以下内容可能不属于 SO 问题;如果超出范围,请随时告诉我离开。这里的问题基本上是,“我是否正确理解了 C 标准,这是处理事情的正确方式吗?”

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

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

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

说到C中的字符处理,有两组分别与可移植性和序列化相关:

wchar_tsetlocale()mbsrtowcs()/wcsrtombs()C 标准没有提到“编码”;事实上,它与任何文本或编码属性完全无关。它只说“你的入口点是main(int, char**);你会得到一个类型wchar_t,它可以保存你系统的所有字符;你可以获得读取输入字符序列并将它们变成可用的wstrings的函数,反之亦然。

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

具有wchar_t 可移植字符类型的可移植、与编码无关的C 世界与确定性外部世界之间的桥梁是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 这样的专用库。)

更新

在许多非常好的 cmets 之后,我想补充几点意见:

如果您的应用程序明确地想要处理 Unicode 文本,您应该将 iconv-conversion 作为核心的一部分,并在 UCS-4 内部使用 uint32_t/char32_t-strings。

Windows:虽然使用宽字符串通常没问题,但与控制台(就此而言,任何控制台)的交互似乎是有限的,因为似乎不支持任何合理的多字节控制台编码和mbstowcs 基本上是无用的(除了微不足道的扩大)。从例如 Explorer-drop 接收宽字符串参数以及 GetCommandLineW+CommandLineToArgvW 可以工作(也许应该有一个单独的 Windows 包装器)。

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

【问题讨论】:

虽然我不认为wmain 应该是extern "C" 如果它需要一个std::vector。 (我认为您不应该将 C++ 类传递给具有 C 链接的函数。) “你得到了一个 wchar_t 类型,它可以保存你系统的所有字符”——不,比这更糟。在 Windows 中,wchar_t 可能只包含代理对的一半。对于这些字符,您需要两个 wchar_t 对象来包含整个字符。还可能会更糟糕的。如果我没记错的话,一个令人讨厌但合法的实现可能会使 wchar_t 与 unsigned char 相同。 是的,代理不是字符,这就是为什么你没有得到可以容纳系统所有字符的 wchar_t 类型的原因。 如果定义了__STDC_ISO_10646__,则wchar_t 值是Unicode 代码点。 C1x 有__STDC_UTF_16____STDC_UTF_32__ 分别对应char16_tchar32_t,C++0x 似乎没有这最后两个宏。 只有一句话要说:阅读utf8everywhere.org 了解如何、为什么、有多冷、为什么会发生、现在该做什么以及其他人应该做什么。 【参考方案1】:

这是仅使用纯标准 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 的程序都必须主动忽略命令行参数、文件和控制台 I/O 或文件和目录操作等 C 和 C++ 标准。这当然不是惯用的。请改用 Boost.Filesystem 或 Qt 等 Microsoft 扩展或包装器。 可移植性 很难实现,尤其是对于 Unicode 支持。你真的必须做好准备,你认为你知道的一切都可能是错误的。例如,您必须考虑到您用于打开文件的文件名可能与实际使用的文件名不同,并且两个看似不同的文件名可能代表同一个文件。创建两个文件 ab 后,您可能会得到一个文件 c 或两个文件 de,它们的文件名与您传递给操作系统的文件名不同。要么您需要一个外部包装库,要么需要大量 #ifdefs。 编码不可知性通常在实践中不起作用,特别是如果您想要便携。您必须知道 wchar_t 在 Windows 上是一个 UTF-16 代码单元,而在 Linux 上 char 通常(机器人并不总是)是一个 UTF-8 代码单元。编码感知通常是更理想的目标:确保您始终知道您使用哪种编码,或者使用将它们抽象出来的包装库。

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

【讨论】:

@Kerrek:不,wmain 不是 main 的包装器,main 不适用于 Unicode。使用 Microsoft 运行时的 Windows 控制台应用程序的真正入口点是_wmainCRTStartup,它通过GetCommandLineW 获取命令行,对其进行解析,然后调用wmain @Kerrek:关于文件名。 Windows 使用 UTF-16 作为文件名(以及其他所有文件名),但您不能使用 fopen 来访问它们。你必须使用_wfopen,这是非标准的。如果你真的想要一个可移植的 C 或 C++ 程序,你不能在 Windows 上支持 Unicode,我认为现在这很难接受。所以最好忘记便携性...... @Kerrek:我不认为 C 标准对文件名有任何说明。是的,如果您尝试打开名称在当前遗留编码(“ANSI 代码页”)中无法表示的任何文件,来自 Microsoft C 运行时的fopen 将不起作用。本质上,这意味着fopen 不可用。 是的,您可以使用_wfopen 打开任何文件:这就是它的用途。但它是特定于 Windows 的。对于跨平台代码,您需要编写一个函数,在 Windows 上调用 _wfopen,在其他系统上调用 fopen 不同意使用 wchar_t 的建议。我认为 char 更适合 unicode 支持。我的观点总结在 utf8everywhere.org。【参考方案2】:

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

务必定义函数以在 UTF-8、UTF-16 和 UTF-32 函数之间进行转换。

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

【讨论】:

我认为“平台相关”和“便携”的含义不同。我不想在 PC、Mac 和 Playstation 之间交换我的 RAM 内容,我只想让程序在每个平台上编译和运行。理想情况下,我根本不想知道 any 编码!我唯一需要担心编码是在序列化/反序列化阶段,这是我使用iconv() 进行接口的地方。在内部,我不想知道关于我的数据表示的任何事情。那有意义吗?就像 C 的基本座右铭一样,“价值,而不是代表”。 另外,根据您的推理,int 是平台相关的,因为它在这里是 32 位,那里是 64 位——是的,类型在不同平台上可能有不同的范围,但这并不意味着某些东西不可移植- 它只是让它表现不同。例如。 Windows XP 不允许我使用非 BMP unicode 字符,但 Linux 可以。美好的。这就是你作为本地人所得到的。 UTF-32 对于 Linux 来说并不是真正的“原生”,就像 UTF-16 对于 Windows 的方式一样:所有 POSIX API 函数(与宽字符处理无关)都使用 @987654328 @字符串。 Windows API 是另一回事。它的 MultiByte* 函数实际上告诉您它们生成 Unicode。我,我只对标准C感兴趣。我相信&lt;wchar.h&gt; 确实提供了所有标准功能的广泛版本,例如wcstoulwcscmp 等。没有 encoding 是原生的,因为语言标准没有讨论 i/o 序列化格式。【参考方案3】:

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

使用wchar_t 比只选择一种 UTF 编码要困难得多:

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

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

文本编辑器:如何在wchar_t 字符串中找到字素簇边界?

如果我知道字符串的编码,我可以直接检查字符。如果我不知道编码,我必须希望我想对字符串做的任何事情都由某个库函数实现。所以wchar_t 的可移植性有点无关紧要,因为我不认为它是一种特别有用 的数据类型。

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

【讨论】:

好点,我认为您在这里确实遇到了问题,这完全取决于您要对数据做什么。如果显式 unicode 文本处理是核心部分,那么无论如何转换为 UTF32 作为主要内部程序应该是核心的一部分,而不是 I/O(即输入是 mbsrtowcs -> iconv(WCHAR_T -> UTF32); 输出是相反的)。只需相应地调整我上面的 ASCII 艺术图表... ... 另一方面,如果文本字符串在您的程序中仅扮演辅助角色(例如,在最终比分屏幕上打印球员姓名),那么将我们限制为可用的系统字符是完全合理的.关于 HTML:您必须知道页面的编码!如果是 UTF32,那么只需在 U"\65536" 上执行 iconv(UTF32->WCHAR_T);它要么工作要么失败。您的 Text 和 JS 示例明确要求显式处理 Unicode,请参见上文。 (文本示例甚至可能需要复杂的 unicode 内容,例如参见 libicu。) 另外,我同意在不知道其编码的情况下抽象“字符串”类型的实用性可能相当有限。但是我绝对可以做的是比较和匹配,即使是 L"foo" 这样的文字常量,所以我认为在很多情况下我需要 some 类型的字符串处理,但我永远不需要知道关于编码的细节——例如从标准输入读取内容,为每个分配座位号并将结果输出到标准输出。 @Kerrek:虽然您并不总是需要知道您正在使用哪种编码,但很难预测这是否适用于您的项目。选择特定的编码(UTF-8/16/32)相对安全,除了一些特定于平台的 API,我看不到 wchar_t 有什么好处。如果您认为可移植程序(根据规范)不允许假设 wchar_t 可以存储任意 Unicode 字符串,即使在转换后也是如此。 我想这实际上是有道理的。我想理论上有可能您的环境使用了您不知道也无法制作的完全晦涩的编码,因此您需要使用wcstombs 来创建可用的输出,并且您需要通过内部@987654334 @-细绳。但实际上,当语言环境使用 UTF8 时,内部 16 位 wchar_t 表示确实会不必要地限制您。我认为我真正的问题是,如果不是通过mbstowcs,我应该如何处理标准输入数据。【参考方案4】:

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

char32_tchar16_t 附带了新的 codecvt 构面,所以我看不出你怎么会出错,只要你保持一致并选择一种字符类型 + 编码(如果构面在这里)。

22.5 [locale.stdcvt](来自 n3242)中描述了这些方面。


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

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'LIT"Hello, World!" 不计后果地放弃,而不知道底层表示是什么。然后在需要时使用from_interfaceX(some_string)。它也不影响全局语言环境或流。助手可以根据需要变得聪明,例如codecvt_utf8 可以处理“标题”,我认为这是来自 BOM 等棘手内容的标准语言(同上 codecvt_utf16)。

事实上,我写上面的内容是为了尽可能短,但你真的想要这样的助手:

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* 或范围。

【讨论】:

iconv 不能是“纯标准”,因为纯标准根本没有编码的概念。这就是为什么我只想在 i/o 接口端使用 iconv 的原因。理想情况下,我不想在内部“选择一种编码”,因为编码不是编程概念——它们是序列化概念。虽然我没有序列化,但如果我不得不提到显式编码,我会觉得很脏。 你是什么意思,提一下?您可以将其重构为例如一个 typedef (但你仍然必须满足于给定的文字,除非使用宏)。与某些东西交互时,无论需要什么转换,都会选择正确的重载。如果您觉得“编码不是编程概念”,那么为什么不选择 UTF-32? “提及”我的意思是,如果我写'a'L'a',我会得到“字符'a'”,但我绝对没有权利猜测它是如何实现的(特别是它的整体为 97)。 所有我保证char可以持有'a'和wchar_t 持有L'a'。没有 typedef,没有选择,没有编码。只是字符“a”。 @Kerrek 看了一圈后,虽然可以从(char,窄编码)转换为(wchar_t,宽编码),也可以从任何([char, char16_t, char32_t], [utf-8, utf-16, utf-32]) 对几乎任何其他,标准没有提供从实现编码到 Unicode 编码并返回的方法。我不会挽救这个答案,我推荐 Philipp 的。 伙计们 - 您知道我们有一个出色的聊天功能,您可以在其中进行有趣的讨论。 :)

以上是关于WChars、编码、标准和可移植性的主要内容,如果未能解决你的问题,请参考以下文章

嵌入式的可移植性和可复用性

StreamReader 和可移植类库

功能实现了就行了?需要多考虑代码的可移植性和可复用性!

Visual Studio中类库和可移植类库的区别

ASP.NET MVC 3、Razor 视图和可移植区域

寻找一种更有效(和可移植)的方式在 unix 中获取(数字)文件权限