正确的方式将宽字符串打印到具有不同编码的终端

时间:2019-01-05 10:50:57

标签: c++ character-encoding cout widechar

下面,我将尝试将字符串(拉丁语“ ex”,西里尔字母“ ya”和腓尼基语“ teth”)打印到具有各种编码(即utf8,cp1251和C(POSIX))的终端。我希望在utf8终端中看到,在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::coutstd::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)。

0 个答案:

没有答案