Форум программистов, компьютерный форум, киберфорум
talis
Войти
Регистрация
Восстановить пароль
Карта форума Блоги Сообщество Поиск Заказать работу  
Рейтинг: 5.00. Голосов: 2.

О кодировках, локалях, фасетах и русском тексте в C++.

Запись от talis размещена 08.10.2012 в 11:52
Обновил(-а) talis 08.10.2012 в 20:21

Некоторые из форумчан, с которыми мы общались на эту тему, возможно, помнят, что некоторое время назад мне была интересна тема обработки русских символов в 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++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#include <iostream>
#include <string>
#include <deque>
#include <algorithm>
#include <iterator>
#include <sstream>
#include <locale>
 
namespace utils
{
 
/*
 *  Костыль для каста объектов других типов в std::wstring
 */
class wformat
{
protected:
    std::wstringstream ss;
 
public:
    template <typename T>
    wformat &
    operator<< ( const T &other )
    {
        ss << other;
        return *this;
    }
 
    operator std::wstring()
    {
        return ss.str();
    }
};
 
} // namespace utils
 
 
 
int
main()
{
    /* 
     * По-умолчанию, даже на UTF8-системах (например, у меня на работе CentOS_5-8),
     * если не указано иного, используется "классическая" кодировка (C Locale)
     * Указываем везде использовать UTF-8
     */
    std::locale::global( std::locale( "ru_RU.UTF8" ) );
 
    /* 
     * По-умолчанию стандартные потоки C++ (std::cin, std::cout, std::cerr)
     * работают как врапперы вокруг stdin, stdout и stderr, и не используют
     * фасеты для преобразования кодировок. Чтобы задействовать фасеты,
     * необходимо отключить синхронизацию со стандартными потоками ввода-вывода.
     */
    std::ios_base::sync_with_stdio( false );
 
 
 
 
 
    typedef std::deque<std::wstring> string_list;
    
    string_list user_strings;
    
    std::wcout << L"Вводите текст. Введите пустую строку для прекращения.\n";
    
    {
        std::wstring input;
 
        /*
         * Читаем ввод, пока не получим пустую строку или EOF,
         * и запихиваем прочтённые строки в контейнер, попутно
         * приписывая им их длину и последние два символа.
         */
        while( std::wcout << L"> ", std::getline( std::wcin, input ), input != L"" )
        {
            user_strings.push_back(
                  utils::wformat()
                  << input.length()
                  << L": "
                  << input
                  << L" ("
                  << ( ( input.length() > 2 ) ? ( input.substr( input.length() - 2 ) ) : std::wstring( L"" ) )
                  << L')'
                  );
        }
    }
    
    /*
     * Выводим содержимое контейнера, используя стандартный алгоритм copy и std::ostream_iterator, на
     * стандартный вывод
     */
    std::copy( user_strings.begin(), user_strings.end(), std::ostream_iterator<std::wstring, wchar_t> ( std::wcout, L"\n" ) );
    
    return 0;
}
Пример тестировался на CentOS 5.8 x86 с g++ 4.4 с системной локалью ru_RU.UTF8.

-----

Разумеется, это всего лишь один из методов работы с русским текстом. Из всех известных мне методов он выглядит наименее костыльным. С полным пришествием c++11 и char32_t, я уверен, все эти прелести с wchar_t, зависящим от компилятора, канут в лету, и мы будем иметь дело с каким-нибудь std::basic_string< char32_t >.
Размещено в Без категории
Показов 17478 Комментарии 5
Всего комментариев 5
Комментарии
  1. Старый комментарий
    Аватар для soon
    Я бы приписывал один символ - не было бы исключения при вводе менее двух символов. Или же проверял бы длинну строки. Но это мелочи
    Запись от soon размещена 08.10.2012 в 17:25 soon вне форума
  2. Старый комментарий
    Аватар для talis
    soon, в нашем деле важна каждая мелочь :-) Вы про какой участок кода?
    Запись от talis размещена 08.10.2012 в 19:03 talis вне форума
  3. Старый комментарий
    Аватар для soon
    Цитата:
    C++
    1
    
    input.substr( input.length() - 2 )
    Если в строке меньше двух символов, выкинет исключение out_of_range
    Запись от soon размещена 08.10.2012 в 20:09 soon вне форума
  4. Старый комментарий
    Аватар для talis
    Хм, спасибо, не заметил :-) В любом случае, смысл примера в другом: показать, что length() и substr() корректно обрабатывают unicode-символы. Поправлю :-)
    Запись от talis размещена 08.10.2012 в 20:11 talis вне форума
  5. Старый комментарий
    Аватар для soon
    Да, я сразу сказал - мелочь
    Запись от soon размещена 08.10.2012 в 20:14 soon вне форума
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2024, CyberForum.ru