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

|| 以下内容可能不符合SO问题;如果超出范围,请随时告诉我离开。这里的问题基本上是:“我是否正确理解C标准,这是正确的方法吗?” 我想对我对C(以及C ++和C ++ 0x)中的字符处理的理解进行澄清,确认和更正。首先,一个重要的观察: 可移植性和序列化是正交的概念。 便携式事物是诸如C 、,0ѭ,
wchar_t
之类的东西。可序列化的东西是诸如
uint32_t
或UTF-8之类的东西。 \“ Portable \”表示您可以重新编译相同的源并在每个受支持的平台上获得有效的结果,但是二进制表示形式可能完全不同(或什至不存在,例如TCP-over-carrier鸽)。另一方面,可序列化的事物始终具有相同的表示形式,例如我可以在Windows桌面,手机或牙刷上阅读的PNG文件。可移植事物是内部的,可序列化的事物,用于处理I / O。可移植的东西是类型安全的,可序列化的东西需要类型修剪。 对于C语言中的字符处理,有两类分别与可移植性和序列化有关:
wchar_t
setlocale()
mbsrtowcs()
/
wcsrtombs()
:C标准对“编码”一无所知;实际上,它与任何文本或编码属性完全无关。它只说“您的入口点是ѭ7”;您会得到类型
wchar_t
,它可以容纳系统的所有字符;您会获得读取输入字符序列并使它们变为可用的字符串的功能,反之亦然。
iconv()
和UTF-8,16,32:一种函数/库,用于在定义良好的,确定的固定编码之间进行代码转换。除了一个例外,iconv处理的所有编码均得到普遍理解和同意。 C的可移植的,与编码无关的世界(其“ 1”个可移植字符类型)与确定性的外部世界之间的桥梁是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
-转换部分作为核心部分,并在UCS-4内部使用
uint32_t
/
char32_t
字符串。 Windows:虽然通常使用宽字符串是可以的,但与控制台(就此而言,任何控制台)的交互似乎受到限制,因为似乎不支持任何明智的多字节控制台编码,并且
mbstowcs
本质上是无用的(除了用于微不足道的扩展)。从Explorer-drop和
GetCommandLineW
+
CommandLineToArgvW
一起接收宽字符串参数(也许对于Windows应该有一个单独的包装器)。 文件系统:文件系统似乎没有任何编码概念,只是将任何以null终止的字符串作为文件名。大多数系统采用字节字符串,但是Windows / NTFS采用16位字符串。在发现存在哪些文件以及处理该数据时(例如,不构成有效UTF16的
char16_t
序列(例如,裸代理)是有效的NTFS文件名),您必须格外小心。 Standard C
fopen
无法打开所有NTFS文件,因为没有可能的转换将映射到所有可能的16位字符串。可能需要使用Windows专用的ѭ23。作为推论,通常没有明确定义“多少字符组成一个给定文件名”的概念,因为首先没有“字符”的概念。买者自负。     
已邀请:
这是仅使用纯标准C / C ++编写惯用的,可移植的,通用的,与编码无关的程序核心的正确方法吗? 不,至少没有任何方法可以满足所有这些属性,至少如果您希望程序在Windows上运行。在Windows上,您几乎必须在所有地方都忽略C和C ++标准,并且只能使用
wchar_t
(不一定在内部使用,而在系统的所有接口上使用)。例如,如果您以
int main(int argc, char** argv)
您已经失去了对命令行参数的Unicode支持。你必须写
int wmain(int argc, wchar_t** argv)
而是使用C标准中未指定的
GetCommandLineW
函数。 进一步来说, Windows上任何支持Unicode的程序都必须主动忽略C和C ++标准,例如命令行参数,文件和控制台I / O或文件和目录操作。这当然不是惯用语。请使用Microsoft扩展程序或包装程序,例如Boost.Filesystem或Qt。 很难实现可移植性,尤其是对于Unicode支持。您确实必须做好准备,以使您认为自己所知道的一切都可能是错误的。例如,您必须考虑到用于打开文件的文件名可能与实际使用的文件名不同,并且两个看似不同的文件名可能代表同一文件。创建两个文件a和b后,可能会得到一个文件c或两个文件d和e,它们的文件名与传递给OS的文件名不同。您需要一个外部包装库或许多28英镑的包装。 不可知论的编码通常在实践中是行不通的,特别是如果您想携带便携式的话。您必须知道
wchar_t
在Windows上是UTF-16代码单元,
char
在Linux上通常(不是总是)是UTF-8代码单元。编码意识通常是更可取的目标:确保始终知道您使用哪种编码,或者使用将其抽象化的包装器库。 我想我必须得出一个结论,除非您愿意使用其他库和特定于系统的扩展,并为此付出了很多努力,否则用C或C ++构建可移植Unicode的应用程序是完全不可能的。不幸的是,大多数应用程序已经无法完成相对简单的任务,例如“向控制台写入希腊字符”或“以正确的方式支持系统允许的任何文件名”,而这些任务只是迈向真正的第一步。 Unicode支持。     
        我会避免使用“ 1”类型,因为它与平台有关(根据您的定义,不是“可序列化”):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那样编写每个字符串函数的重载窄/宽版本。选择一种首选编码以供内部使用,并坚持使用。对于需要不同编码的内容,请根据需要进行转换。     
        
wchar_t
的问题在于,与编码无关的文本处理太困难了,应该避免。如果您坚持使用“纯C”,则可以使用所有
w*
函数(例如
wcscat
和朋友),但是如果您想做任何更复杂的事情,则必须潜入深渊。 如果您只选择一种UTF编码,则使用
wchar_t
进行的某些事情比它们要难得多: 解析Javascript:标识符可以在BMP之外包含某些字符(并假设您关心这种正确性)。 HTML:如何将
&#65536;
转换为
wchar_t
的字符串? 文本编辑器:如何在
wchar_t
字符串中找到字素簇边界? 如果我知道字符串的编码,则可以直接检查字符。如果我不知道编码,我必须希望我想对字符串做的任何事情都由某个地方的库函数实现。因此ѭ1的可移植性与我无关,因为我认为它不是特别有用的数据类型。 您的计划要求可能会有所不同,ѭ1可能对您有效。     
鉴于ѭ15不是“纯标准C / C ++”,我认为您不满足自己的规范。 new17ѭ和
char16_t
附带了新的ѭ46ts构面,因此,只要您保持一致,并选择一种char类型+编码(如果这些构面在此处),我就看不到怎么可能出错。 有关方面,请参见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::string
ns::char_t
LIT\'A\'
LIT\"Hello, World!\"
,而无需知道底层的表示形式。然后在需要时使用
from_interfaceX(some_string)
。它也不影响全局语言环境或流。助手可以根据需要而变得聪明,例如
codecvt_utf8
可以处理\'headers \',我认为这是来自BOM表(同上stuff56 Standard)之类棘手东西的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*
或范围。     

要回复问题请先登录注册