Ниже я попытаюсь напечатать строку XЯ?
(латинский «ex», кириллический «ya» и финикийский «teth») на терминалах с различными кодировками, а именно utf8, cp1251 и C (POSIX). Я ожидаю увидеть XЯ?
в терминале utf8, XЯ?
в терминале cp1251 и X??
в терминале C (POSIX). Вопросительные знаки объясняются тем, что выходная библиотека 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.
(2) Вторая попытка была вручную вызвать std::setlocale()
с кодировкой utf8:
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
переменную окружения для фактического кодирования, используемого терминалом (и надеяться, что все части терминала используют одну и ту же кодировку ):
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??
Теперь вывод во всех трех терминалах был таким, как я ожидал. Однако смешивание 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).