Как я могу использовать std:: imbue для установки локали для std:: wcout?

Я пытаюсь использовать механизм std::locale в С++ 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";
}

Ответы

Ответ 1

В этом ответе я беру вопросы в обратном порядке и добавляю еще один (с ответом), который появился на этом пути.

Есть ли способ использовать imbue вместо того, чтобы устанавливать глобальную локаль, чтобы делать то, что я хочу?

Да. По умолчанию std::wcout синхронизируется с базовым потоком stdout C. Поэтому std::wcout может использовать imbue, если эта синхронизация отключена, позволяя потоку С++ работать независимо. Поэтому, чтобы изменить исходный код для использования 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>

Кроме того, без явной установки глобальной локали, locale является языковым стандартом "C", который является US English ASCII, поэтому это означает, что stdout будет по умолчанию иметь ASCII-сопоставление. Поскольку в ASCII не представлены кириллические символы, базовый stdout - это то, что преобразует правильный русский в последовательность символов ?.

Почему перед вызовом sync_with_stdio предшествует imbue?

В соответствии с 27.5.3.4 стандарта:

Если какая-либо операция ввода или вывода произошла с использованием стандартных потоков до вызова, эффект определяется реализацией. В противном случае, вызываемый с ложным аргументом, он позволяет стандартным потокам работать независимо от стандартных потоков C.

Ответ 2

Сначала я сделал еще несколько тестов, используя ваш код, и я могу подтвердить, что 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, которая ничего не знает о юникоде. Все печатные символы ASCII (ниже 128) выводятся как есть, а другие заменяются на ?. Это именно то, что делает ваша программа.

Чтобы он работал правильно, вам нужно выбрать локаль, которая знает о символах Unicode с помощью 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, "");. Он автоматически адаптируется к текущей кодировке. Я тестировал с урезанным вариантом с использованием латинского набора символов (моя система говорит по-французски, а не по-русски...)

#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) (resp export LANG=fr_FR.UTF-8 и export LANG=fr_FR.ISO-8859-1), и я правильно получил èé в правильной кодировке. Я попробовал это также под Windows XP, с кодовой страницей 851 (oem) и 1252 (ansi) (соответственно chcp 850 и chcp 1252 с консолью Lucida console) и получил èé на консоли.

Изменить 2:

Конечно, вы также можете установить глобальную локаль С++ с locale::global(locale(""); с локалью по умолчанию или locale::global(locale("ru_RU.UTF-8"); с русской локалью, но это больше, чем просто вызов setlocale. Согласно документации по реализации Gnu стандартной библиотеки С++ о locale: существует только одно отношение (языкового механизма С++) к языковому механизму C: глобальное C изменен, если названный объект локали С++ задан как глобальная локаль ", то есть: std::locale::global(std::locale("")); влияет на функции C, как если бы был выполнен следующий вызов: std::setlocale(LC_ALL, "");. С другой стороны, нет наоборот, то есть вызов setlocale не имеет никакого отношения к языковому механизму С++, в частности по работе с locale (" ").

Итак, похоже, что существует базовый механизм библиотеки C, который должен быть сначала включен с помощью setlocale, чтобы позволить преобразованию imbue работать правильно.

Ответ 3

Я не знаю, на каких языках вы планируете поддерживать, но есть языки, где ваш алгоритм не применяется, например. Японский. Я предлагаю проверить итераторы слова в международных компонентах для Unicode. http://userguide.icu-project.org/boundaryanalysis