Как я могу использовать 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Я получаю ошибку, упомянутую в этом другом вопросе.

Те, кто интересуется экспериментированием с кодом, с настройками компилятора или с обоими, могут захотеть использовать этот живой код.

Мои вопросы

  1. Почему это не работает? Это потому что wcout уже открыт?
  2. Есть ли способ использовать 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

Другие вопросы по тегам