下面,我将尝试将字符串XЯ
(拉丁语“ ex”,西里尔字母“ ya”和腓尼基语“ teth”)打印到具有各种编码(即utf8,cp1251和C(POSIX))的终端。我希望在utf8终端中看到XЯ
,在cp1251终端中看到XЯ?
,在C(POSIX)终端中看到X??
。问号是因为C ++输出库用?
替换了它不能表示的字符。这是正确的预期行为。
(1)我的第一次尝试是将宽字符串打印到wcout:
wchar_t str[] = L"\U00000058\U0000042f\U00010908";
std::wcout << str << std::endl;
// utf8 terminal output: X??
// cp1251: X??
// C: X??
在所有终端中,它仅正确打印了第一个字符ascii7。其他字符被替换为“?”分数。事实证明,发生这种情况是因为在程序启动期间,LC_ALL是C的set。
(2)第二次尝试是使用utf8编码手动调用std::setlocale()
:
wchar_t str[] = L"\U00000058\U0000042f\U00010908";
std::setlocale(LC_ALL, "en_US.UTF-8");
std::wcout << str << std::endl;
// utf8: XЯ
// cp1251: XЯ𐤈
// C: XЯð¤
很明显,这在utf8终端中可以正常工作,但是在其他两个终端中则造成了垃圾。
(3)第三次尝试是解析$LANG
环境变量以获取终端使用的实际编码(并希望终端use the same encoding的所有片段):
const char* lang = std::getenv("LANG");
if (!lang) {
std::cerr << "Couldn't get LANG" << std::endl;
exit(1);
}
wchar_t str[] = L"\U00000058\U0000042f\U00010908";
std::setlocale(LC_ALL, lang);
std::wcout << str << std::endl;
// utf8: XЯ
// cp1251: XЯ?
// C: X??
现在所有三个端子的输出都与我预期的一样。但是,mixing std::cout
和std::wcout
是一个坏主意,std::cout
肯定是我程序中使用的某些第三方库使用的。这使std::wcout
无法使用。
(4)因此,第四次尝试(或实际上是,构想)是从$LANG
中检测终端编码,使用codevct()
将wchar_t[]
字符串转换为终端编码,并用普通std::cout.write()
。不幸的是,我找不到为codevct()
显式设置目标编码的方法。
(5)到目前为止,第五次尝试是手动使用iconv()
:
// get $LANG env var
const char* lang = std::getenv("LANG");
if (!lang) {
std::cerr << "Couldn't get $LANG" << std::endl;
exit(1);
}
// find out encoding from $LANG, e.g. "utf8", "cp1251", etc
std::string enc(lang);
size_t pos = enc.rfind('.');
if (pos != std::string::npos) {
enc = enc.substr(pos + 1);
}
if (enc == "C" || enc == "POSIX") {
enc = "iso8859-1";
}
// convert wchar_t[] string into terminal encoding
wchar_t str[] = L"\U00000058\U0000042f\U00010908";
iconv_t handler = iconv_open(enc.c_str(), "UTF32LE");
if (handler == (iconv_t)-1) {
std::cerr << "Couldn't create iconv handler: " << strerror(errno) << std::endl;
exit(1);
}
char buf[1024];
char* inbuf = (char*)str;
size_t inbytes = sizeof(str);
char* outbuf = buf;
size_t outbytes = sizeof(buf);
while (true) {
size_t res = iconv(handler, &inbuf, &inbytes, &outbuf, &outbytes);
if (res != (size_t)-1) {
break;
}
if (errno == EILSEQ) {
// replace non-convertable code point with question mark and retry iconv()
inbuf[0] = '\x3f';
inbuf[1] = '\x00';
inbuf[2] = '\x00';
inbuf[3] = '\x00';
} else {
std::cerr << "iconv() failed: %s" << strerror(errno) << std::endl;
exit(1);
}
}
iconv_close(handler);
// write converted string to std::cout
std::cout.write(buf, sizeof(buf) - outbytes);
std::cout << std::endl;
// utf8: XЯ
// cp1251: XЯ?
// C: X??
这在所有三个终端中均正常工作。现在我也不怕在程序的其他部分使用std::cout
。但是,我发现此解决方案不是C ++方式。
因此,问题是:在C ++中打印宽字符串的正确方法是什么?我可以使用特定于平台的解决方案(Linux + glibc + GCC)。