О кодировках, локалях, фасетах и русском тексте в C++.
Некоторые из форумчан, с которыми мы общались на эту тему, возможно, помнят, что некоторое время назад мне была интересна тема обработки русских символов в C++. Кажется, я, наконец, нашёл решение. C++ уже всё умеет делать, всё есть под капотом. Нужно просто знать, как этим пользоваться. Этому и посвящена моя первая запись в блоге. Потоки ввода-вывода C++ внутри себя используют локали для преобазования текста между кодировками. Локали, в свою очередь, содержат фасеты, которые используются потоками, когда тем необходимо совершить преобразования, зависящие от локали. Помимо преобразований формата дат, денежных единиц, чисел с плавающей точкой и прочего, локали (через фасеты) диктуют правила сравнения строк и классификации символов (буква / цифра / пунктуация / непечатный, верхний / нижний регистр и т. п.). По-умолчанию все C++-программы использую классическую локаль C (C Locale). Это означает, что для того, чтобы вся подкапотная магия заработала, нам нужно указать локаль системы. Можно либо назначить конкретную локаль конкретному потоку (std::istream::imbue, std::ostream::imbue, так же для производных классов), либо выставить глобальную локаль (которую, впрочем, потом всё равно можно переопределить для отдельного потока через imbue), через std::locale::global. Однако, на этом песня не заканчивается. В Стандарте сказано, что std::cin, std::cout и std::cerr, а также менее известный std::clog, являются "расширениями" соответствующих сишных потоков (stdin, stdout, stderr...). По-этому стандартные потоки C++ синхронизируются с сишными потоками. Это, конечно, очень хорошо, особенно для любителей смешивать плюсовый код с сишным. Однако, для того, чтобы потоки начали работать с фасетами, синхронизацию прийдётся отключить через std::ios_base::sync_with_stdio( false ); И даже это ещё не всё. То, что std::string способна хранить UTF-8 строки, ещё не означает, что она их поддерживает. Русские символы в utf-8 занимают два байта, а не один, и по-этому длина строки "привет" из 6 букв с точки зрения std::string будет равна 12 символам. А вот поддерживает unicode строки std::wstring, которые в качестве типа символа используют не char_t, а wchar_t. Тут надо заметить, что размер wchar_t тоже implementation-defined, то есть зависит от компилятора. И если уж делать ну совсем хорошо, нужно думать в сторону char32_t. Однако, в эту сторону я пока ещё не смотрел. Для работы с std::wstring также понадобятся потоки, которые поддерживают std::wstring. Для стандартных потоков ввода-вывода это std::wcin, std::wcout и std:wcerr. Для файловых потоков существуют классы std::wifstream, std::wofstream и std::wfstream, которые наследуются от std::wistream и std:wostream. Это означает, что для ввода/вывода объекта пользовательского типа в поток нужно будет определить operator<< и operator>> (или, возможно, поля класса, в зависимости от дизайна) для wide-потоков и этого пользовательского класса. Однако, для std::ostream_iterator и std::istream_iterator таких аналогов нет. Не очень красиво, но ничего страшного: вторым шаблонным параметром является тип символа, CharTraits. Для std::wstring эти итераторы будыт выглядеть как std::ostream_iterator< std::wstring, wchar_t >. В завершение статьи приведу пример, демонстрирующий корректную обработку русского текста в unicode некоторыми методами std::wstring, стандартными итераторами и потоками:
----- Разумеется, это всего лишь один из методов работы с русским текстом. Из всех известных мне методов он выглядит наименее костыльным. С полным пришествием c++11 и char32_t, я уверен, все эти прелести с wchar_t, зависящим от компилятора, канут в лету, и мы будем иметь дело с каким-нибудь std::basic_string< char32_t >. |
Всего комментариев 5
Комментарии
-
Запись от soon размещена 08.10.2012 в 17:25 -
Запись от talis размещена 08.10.2012 в 19:03 -
Запись от soon размещена 08.10.2012 в 20:09 -
Запись от talis размещена 08.10.2012 в 20:11 -
Запись от soon размещена 08.10.2012 в 20:14