Как я могу использовать std::imbue, чтобы установить локаль для std::wcout?
Я пытаюсь использовать std::locale
механизм в C++11 для подсчета слов на разных языках. Конкретно у меня есть std::wstringstream
который содержит название известного русского романа ("Преступление и наказание" на английском языке). Что я хочу сделать, это использовать соответствующую локаль (ru_RU.utf8
на моей машине с Linux), чтобы прочитать поток строк, посчитать слова и распечатать результаты. Я также должен отметить, что моя система настроена на использование en_US.utf8
локали.
Желаемый результат таков:
0: "Преступление"
1: "и"
2: "наказание"
I counted 3 words.
and the last word was "наказание"
Это все работает, когда я устанавливаю глобальную локаль, но не когда я пытаюсь imbue
wcout
поток. Когда я пытаюсь это сделать, я получаю такой результат:
0: "????????????"
1: "?"
2: "?????????"
I counted 3 words.
and the last word was "?????????"
Кроме того, когда я пытаюсь использовать решение, предложенное в комментариях, (которое можно активировать, изменив #define USE_CODECVT 0
в #define USE_CODECVT 1
Я получаю ошибку, упомянутую в этом другом вопросе.
Те, кто интересуется экспериментированием с кодом, с настройками компилятора или с обоими, могут захотеть использовать этот живой код.
Мои вопросы
- Почему это не работает? Это потому что
wcout
уже открыт? - Есть ли способ использовать
imbue
вместо того, чтобы устанавливать глобальную локаль, чтобы делать то, что я хочу?
Если это имеет значение, я использую g++ 4.8.3. Полный код показан ниже.
getwords.cpp
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <locale>
#define USE_CODECVT 0
#define USE_IMBUE 1
#if USE_CODECVT
#include <codecvt>
#endif
using namespace std;
int main()
{
#if USE_CODECVT
locale ru("ru_RU.utf8",
new codecvt_utf8<wchar_t, 0x10ffff, consume_header>{});
#else
locale ru("ru_RU.utf8");
#endif
#if USE_IMBUE
wcout.imbue(ru);
#else
locale::global(ru);
#endif
wstringstream in{L"Преступление и наказание"};
in.imbue(ru);
wstring word;
unsigned wordcount = 0;
while (in >> word) {
wcout << wordcount << ": \"" << word << "\"\n";
++wordcount;
}
wcout << "\nI counted " << wordcount << " words.\n"
<< "and the last word was \"" << word << "\"\n";
}
3 ответа
В этом ответе я беру вопросы в обратном порядке и добавляю еще один (с ответом), который возник по пути.
Есть ли способ использовать imbue
вместо того, чтобы устанавливать глобальную локаль, чтобы делать то, что я хочу?
Да. По умолчанию, std::wcout
синхронизируется с основным stdout
С потоком. Так std::wcout
можно использовать imbue
если эта синхронизация отключена, что позволяет потоку C++ работать независимо. Таким образом, чтобы изменить оригинальный код для использования imbue
и работать как задумано, нужно добавить только одну строку, вызывая std::ios_base::sync_with_stdio
:
std::ios_base::sync_with_stdio(false);
std::wcout.imbue(ru);
Почему оригинальная версия не работает?
Стандарт (я имею в виду INCITS/ISO/IEC 14882-2011[2012]) очень мало говорит о связи с базовым stdio
поток, но в 27.4.3 это говорит
Предмет
wcout
управляет выводом в буфер потока, связанный с объектомstdout
заявлено в<cstdio>
Кроме того, без явной установки глобальной локали локаль является "C"
языковой стандарт США ASCII, так что это означает, что stdout
по умолчанию будет иметь отображение ASCII. Поскольку в ASCII нет символов кириллицы, основной stdout
это то, что превращает правильный русский в серию ?
персонажи.
Почему sync_with_stdio
вызов предшествует imbue
?
Согласно 27.5.3.4 стандарта:
Если какая-либо операция ввода или вывода произошла с использованием стандартных потоков до вызова, эффект определяется реализацией. В противном случае, вызывается с ложным аргументом, он позволяет стандартным потокам работать независимо от стандартных C-потоков.
Сначала я сделал еще один тест, используя ваш код, и я могу подтвердить, что L"Преступление и наказание"
является правильной строкой UTF16. Я контролировал код отдельных персонажей, и они правильно 0x41f, 0x440, 0x435, 0x441, 0x442, 0x443, 0x43f, 0x43b, 0x435, 0x43d, 0x438, 0x435, 0x20, 0x438, 0x20, 0x43d, 0x430, 0x43a, 0x430, 0x437, 0x430, 0x43d, 0x438, 0x435
Я не мог найти какую-либо ссылку на это, но похоже, что просто позвонив imbue
недостаточно. imbue
это метод из basic_ios
который является предком cout
а также wcout
, Он действует на числовые преобразования, но во всех моих тестах он не влияет на кодировку, используемую для вывода.
По умолчанию язык, используемый в программе на C++ (или C),... C
язык, который ничего не знает о юникоде. Все печатные символы ASCII (ниже 128) выводятся как есть, а другие заменяются на ?
, Это именно то, что делает ваша программа.
Чтобы заставить его работать правильно, вы должны выбрать локаль, которая знает о символах юникода с setlocale
, Как только это будет сделано, вы можете изменить числовое преобразование, вызвав imbue
, и как вы выбрали кодировку Unicode все будет хорошо.
Таким образом, при условии, что ваша текущая локаль использует кодировку UTF-8, вам нужно только добавить
setlocale(LC_ALL, "");
в первой строке вашей программы, и результат будет таким, как ожидалось:
0: "Преступление"
1: "и"
2: "наказание"
I counted 3 words.
and the last word was "наказание"
Если ваша текущая локаль не использует UTF-8, выберите тот, который установлен в вашей системе и поддерживает его. я использовал setlocale(LC_ALL, "fr_FR.UTF-8");
, или даже setlocale(LC_ALL, "en_US.UTF-8");
и оба работали.
Редактировать:
На самом деле, лучший способ правильно вывести Unicode на экран - это использовать setlocale(LC_ALL, "");
, Он автоматически адаптируется к текущей кодировке. Я тестировал с урезанным вариантом, используя набор символов Latin1 (моя система говорит по-французски, а не по-русски...)
#include <iostream>
#include <locale>
using namespace std;
int main() {
setlocale(LC_ALL, "");
wchar_t ws[] = { 0xe8, 0xe9, 0 };
wcout << ws << endl;
}
Я попробовал это под Linux, используя кодировку UTF-8 и ISO-8859-1 (latin1) (соответственно export LANG=fr_FR.UTF-8
а также export LANG=fr_FR.ISO-8859-1
) и я правильно понял èé
в правильной кодировке. Я попробовал это также под Windows XP, с кодовой страницей 851 (oem) и 1252 (ansi) (соответственно. chcp 850
а также chcp 1252
с Lucida консольной кодировкой) и получил èé
на консоли тоже.
Изменить 2:
Конечно, вы также можете установить глобальный язык C++ с помощью locale::global(locale("");
с локалью по умолчанию или locale::global(locale("ru_RU.UTF-8");
с русским языком, но это больше, чем просто звонок setlocale
, В соответствии с документацией Gnu о реализации стандартной библиотеки C++ по языку: существует только одно отношение (из механизма языка C++) к механизму языка C: глобальный язык C изменяется, если в качестве глобального языка установлен именованный объект языка C++. ", то есть: std::locale::global(std::locale(""));
влияет на функции C, как если бы был сделан следующий вызов: std::setlocale(LC_ALL, "");
, С другой стороны, обратного нет, то есть вызов setlocale не имеет никакого отношения к механизму локали C++, в частности к работе локали ("").
Так что действительно похоже, что был базовый механизм библиотеки C, который должен быть сначала включен с setlocale
позволять imbue
преобразование, чтобы работать правильно.
Я не знаю, какие языки вы планируете поддерживать, но есть языки, к которым ваш алгоритм не применим, например. Японский язык. Я предлагаю проверить итераторы слов в международных компонентах для Unicode. http://userguide.icu-project.org/boundaryanalysis