66

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

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

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

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

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

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

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

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

那么,我是否应该始终将我的字符串内部存储在与编码无关的 wstring 中,通过 与 CRT 接口wcsrtombs()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-conversion 部分作为核心并在 UCS-4 内部使用uint32_t/ -strings。char32_t

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

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

4

4 回答 4

22

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

不,而且根本无法满足所有这些属性,至少如果您希望您的程序在 Windows 上运行的话。在 Windows 上,您几乎必须在任何地方都忽略 C 和 C++ 标准并专门使用wchar_t(不一定在内部,但在系统的所有接口)。例如,如果您从

int main(int argc, char** argv)

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

int wmain(int argc, wchar_t** argv)

取而代之,或者使用GetCommandLineWC 标准中没有指定的函数。

进一步来说,

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

I think I have to conclude that it's completely impossible to build a portable Unicode-capable application in C or C++ unless you are willing to use additional libraries and system-specific extensions, and to put lots of effort in it. Unfortunately, most applications already fail at comparatively simple tasks such as "writing Greek characters to the console" or "supporting any filename allowed by the system in a correct manner", and such tasks are only the first tiny steps towards true Unicode support.

于 2011-06-11T21:18:07.560 回答
9

我会避免使用该wchar_t类型,因为它是平台相关的(根据您的定义不是“可序列化”):Windows 上的 UTF-16 和大多数类 Unix 系统上的 UTF-32。相反,请使用 C++0x/C1x 中的char16_t和/或char32_t类型。(如果您没有新的编译器,请按现在的方式对它们uint16_t进行类型定义uint32_t。)

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

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

于 2011-06-10T01:03:18.740 回答
8

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

wchar_t如果您只选择其中一种 UTF 编码,以下是一些比它们更难处理的事情:

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

  • HTML:你如何&#65536;变成一个字符串wchar_t

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

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

您的程序要求可能会有所不同,并且wchar_t可能适合您。

于 2011-06-11T11:35:05.917 回答
6

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

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

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::string, ns::char_t, LIT'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*或范围之类的内容。

于 2011-06-10T01:37:49.580 回答