WChars,编码,标准和可移植性
以下可能不符合SO的问题; 如果出界,请随时告诉我走开。 这里的问题基本上是,“我是否正确理解C标准,这是正确的方式去做事情?
我想要澄清,确认和更正我对C中字符处理(以及C ++和C ++ 0x)的理解。 首先,一个重要的观察:
可移植性和序列化是正交的概念。
便携式的东西是像C, unsigned int
, wchar_t
。 可串行化的东西是像uint32_t
或UTF-8的东西。 “便携式”意味着您可以重新编译相同的源代码,并在每个支持的平台上获得工作结果,但二进制表示可能完全不同(甚至不存在,例如,TCP-over-carrier鸽子)。 另一方面,可序列化的东西总是具有相同的表示forms,例如我可以在Windows桌面,手机或牙刷上读取的PNG文件。 便携式的东西是内部的,可序列化的东西处理I / O。 便携式的东西是types安全的,可序列化的东西需要types的双关语。 </前导>
在C中的字符处理中,有两组事物分别涉及到可移植性和序列化:
-
wchar_t
,setlocale()
,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文件名),您必须小心。 标准Cfopen
无法打开所有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两个文件a和b之后 ,可能会得到一个文件c或两个文件d和e ,这些文件的文件名与传递给操作系统的文件名不同。 要么你需要一个外部包装库或大量的
#ifdef
。 - 编码不可知性通常在实践中不起作用,特别是如果你想要可移植的话。 您必须知道
wchar_t
是Windows上的UTF-16代码单元,并且该char
在Linux上经常(并非总是)是UTF-8代码单元。 编码意识通常是更可取的目标:确保您始终知道您使用的是哪种编码,或者使用将它们抽象出来的包装器库。
我想我必须得出结论,除非你愿意使用额外的库和系统特定的扩展,并且花费很多精力,否则用C或C ++构build一个可移植的支持Unicode的应用程序是完全不可能的。 不幸的是,大多数应用程序在相对简单的任务中已经失败了,例如“将希腊字符写入控制台”或“以正确的方式支持系统允许的任何文件名”,而这些任务只是实现真正的Unicode支持的第一步。
我会避免wchar_t
types,因为它是依赖于平台的(不是你的定义中的“序列化”):在Windows上是UTF-16,在大多数类Unix系统上是UTF-32。 而是使用C ++ 0x / C1x中的char16_t
和/或char32_t
types。 (如果你没有一个新的编译器,现在就把它们定义为uint16_t
和uint32_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:你如何打开
𐀀
成一个wchar_t
的string? -
文本编辑器:如何在
wchar_t
string中查找字形集群边界?
如果我知道一个string的编码,我可以直接检查字符。 如果我不知道编码,我不得不希望,无论我想用一个string做一个库函数的地方实现。 所以wchar_t
的可移植性有点不相关,因为我不认为它是一个特别有用的数据types。
您的程序要求可能会有所不同, wchar_t
可能适合您。
鉴于iconv
不是“纯粹的标准C / C ++”,我不认为你满足自己的规格。
char32_t
和char16_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::string
, ns::char_t
, LIT'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*
或范围之类的东西。