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

以下可能不符合SO的问题; 如果出界,请随时告诉我走开。 这里的问题基本上是,“我是否正确理解C标准,这是正确的方式去做事情?

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

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

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

在C中的字符处理中,有两组事物分别涉及到可移植性和序列化:

  • wchar_tsetlocale()mbsrtowcs() / wcsrtombs()C标准没有提到“编码” 。 实际上,对任何文本或编码属性都是完全不可知的。 它只是说“你的入口点是main(int, char**) ;你得到一个typeswchar_t ,它可以容纳你所有系统的字符;你可以读取input的字符序列并使它们变成可用的string,反之亦然。

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

C的可移植的,编码不可知的世界与其wchar_t可移植字符types和确定性外部世界之间的桥梁是WCHAR-T和UTF之间的iconv转换

所以,我应该总是将我的string内部存储在一个编码无关的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 ---+ 

实际上,这意味着我会为我的程序入口点编写两个boiler-plate包装器,例如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 + +,以及一个明确的I / O接口到UTF使用iconv? (请注意,像Unicode规范化或变音replace这样的问题超出了范围;只有在您确定自己确实需要Unicode (而不是其他任何编码系统)之后,才能处理这些细节,例如使用专用库像libicu。)

更新

以下很多非常好的评论,我想补充一些意见:

  • 如果您的应用程序明确要处理Unicode文本,则应该使内核的iconv -conversion部分在UCS-4内部使用uint32_t / char32_t -strings。

  • Windows:虽然使用宽string通常很好,但似乎与控制台(任何控制台)的交互是有限的,因为似乎没有支持任何明智的多字节控制台编码,并且mbstowcs本质上是无用的(除了微不足道的扩大外)。 接收来自(比如说,应该有一个单独的Windows封装)一起使用一个资源pipe理器的drop-string和一起使用GetCommandLineW + CommandLineToArgvW宽string参数。

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

这是正确的方式来编写一个惯用的,可移植的,通用的,编码不可知的程序核心只使用纯粹的标准C / C ++

不,并且根本没有办法完成所有这些属性,至less如果您希望您的程序在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,文件和目录操作。 这当然不是惯用的 。 改用微软的扩展或包装,如Boost.Filesystem或Qt。
  • 可移植性非常难以实现,特别是对于Unicode支持。 你必须做好准备,你认为你所知道的一切都可能是错误的。 例如,您必须考虑用于打开文件的文件名可能与实际使用的文件名不同,并且两个看起来不同的文件名可能表示相同的文件。 创build两个文件ab之后 ,可能会得到一个文件c或两个文件de ,这些文件的文件名与传递给操作系统的文件名不同。 要么你需要一个外部包装库或大量的#ifdef
  • 编码不可知性通常在实践中不起作用,特别是如果你想要可移植的话。 您必须知道wchar_t是Windows上的UTF-16代码单元,并且该char在Linux上经常(并非总是)是UTF-8代码单元。 编码意识通常是更可取的目标:确保您始终知道您使用的是哪种编码,或者使用将它们抽象出来的包装器库。

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

我会避免wchar_ttypes,因为它是依赖于平台的(不是你的定义中的“序列化”):在Windows上是UTF-16,在大多数类Unix系统上是UTF-32。 而是使用C ++ 0x / C1x中的char16_t和/或char32_ttypes。 (如果你没有一个新的编译器,现在就把它们定义为uint16_tuint32_t 。)

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

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

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

这里有一些比wchar_t更难的东西,如果你只是select一种UTF编码:

  • parsingJavascript:Identifers可以包含BMP之外的特定字符(并假设您关心这种正确性)。

  • HTML:你如何打开&#65536; 成一个wchar_t的string?

  • 文本编辑器:如何在wchar_tstring中查找字形集群边界?

如果我知道一个string的编码,我可以直接检查字符。 如果我不知道编码,我不得不希望,无论我想用一个string做一个库函数的地方实现。 所以wchar_t的可移植性有点不相关,因为我不认为它是一个特别有用的数据types。

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

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

char32_tchar16_t有新的codecvt方面,所以我不明白你怎么会错,只要你是一致的,并select一个字符types+编码,如果方面在这里。

这些方面在22.5 [locale.stdcvt](从n3242)中描述。


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

 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) 。 它不会影响全球语言环境或stream。 helper可以像需要的那样聪明,例如codecvt_utf8可以处理'headers',我认为这是从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*或范围之类的东西。