Кодировки


    Всем рано или поздно приходится работать с различными кодировками. Заметив в коде своей команды различные, порой странные, подходы к решению этих проблем, пришлось провести разъяснительную беседу. Ниже поделюсь своим видением правильной работы с не-ASCII символами в коде. Буду рад конструктивной критике.



    Принцип работы


    Логика работы с различными кодировками в C++ проста и прозрачна. В общем виде она отражена в схеме. Программа работает в одной — своей внутренней кодировке, а правильно локализованные потоки отвечают за преобразование кодировки данных из внешнего кода во внутренний и обратно. Внутреннюю кодировку программы лучше всего зафиксировать раз и навсегда. Если программа работает с не-ASCII символами, то самый логичный выбор для внутренней кодировки — Юникод, причём использование UTF-8 и char для параметризации STL как правило неоправданно (хотя существуют ситуации, в которых это необходимо); более логично перейти на расширенные символы wchar_t и использовать UCS-2. За преобразование данных из внешней кодировки во внутреннюю отвечает фасет codecvt. Локализованные потоки сами вызовут соответствующие функции фасета при получении данных (о том кто такие фасеты я писал ранее).
    Вышесказанное поясню комментированным примером, в котором прочитаем данные из cp1251 файла, покажем как boost::xpressive работает с юникодом и выведем кириллицу в cout в кодировке cp866 (консоль windows по умолчанию).



    Кодировка исходников


    Прежде чем приступить к рассмотрению примера, следует (на всякий случай) пару слов сказать о кодировке исходных текстов программы. Все свои исходники я держу в UTF-8 (если они содержат широкие строковые константы с не-ASCII символами, то в файлы добавляю BOM), что и всем советую. Современные компиляторы сами преобразуют «широкие» символы, помеченные в исходниках как L"" в UCS-2 (или UCS-4). Понятно, что правильное преобразование зависит от кодировки исходников. gcc по-умолчанию считает, что работает с UTF-8 текстом, чтобы его переубедить придётся специально указывать значение параметра -finput-charset. Компилятору от MS нужно немножечко помочь — добавить в UTF-8 файл BOM (Byte Order Mark). К сожалению у Borland C++ Compiler version 5.5 с UTF-8 проблемы.
    Для тех, кто собрался кинуть в меня камень поясню два момента: первый — мне не удобно читать код с «unicode escape» типа:
    std::wstring wstr(L"\u0410\u0411\u0412\u0413\u0413");
    второй — речь идёт не только об интерфейсе пользователя, поэтому вынести все широкие строковые константы в отдельный модуль и как-то с ними работать (наподобие gettext), не вариант.
    Итак решено — исходники в UTF-8 с BOM. Если кто не знает, в vim BOM можно добавить к файлу с помощью команды «set bomb». Если BOM в файле уже есть vim его никуда не денет.

    Пример работы с различными кодировками


    Ну вот и подобрались к самому интересному. Как я и говорил, код простой и понятный. Маленькое замечание по стандартным потокам — по умолчанию фасеты для них не задействуются так как они синхронизированы с stdio для производительности. Следует указать sync_with_stdio(false).

    #include <boost/xpressive/xpressive.hpp>
    #include <locale>
    #include <fstream>
    #include <iostream>
    #include <iterator>
    
    #include "codecvt_cp866.hpp"
    #include "codecvt_cp1251.hpp"
    #include "unicyr_ctype.hpp"
    
    using namespace std;
    using namespace boost::xpressive;
    
    int main()
    {
      // Пусть имеется файл input.txt в кодировке cp1251, содержащий банальный
      // "Привет, мир!"
      ofstream ofile("input.txt", std::ios::binary);
      ostreambuf_iterator<char> writer(ofile);
      writer   = 0xCF; // П
      ++writer = 0xF0; // р
      ++writer = 0xE8; // и
      ++writer = 0xE2; // в
      ++writer = 0xE5; // е
      ++writer = 0xF2; // т
      ++writer = 0x2C; // ,
      ++writer = 0x20; //  
      ++writer = 0xEC; // м
      ++writer = 0xE8; // и
      ++writer = 0xF0; // р
      ++writer = 0x21; // !
      ofile.close();
    
      // Читаем файл
      wifstream ifile("input.txt");
      // Локаль для ввода
      locale cp1251(locale(""), new codecvt_cp1251<wchar_t, char, mbstate_t>);
      ifile.imbue(cp1251);
      wchar_t wstr[14];
      ifile.getline(wstr, 13);
    
      // Стандарт C++ предписывает синхронизировать cout, cin, cerr и
      // clog, как и их расширенные варианты с stdio, поэтому поумолчанию
      // для этих потоков фасет не будет задействован (во всяком случае при 
      // компиляции gcc, msvc 7 не особо придерживается стандартов). Следует 
      // сообщить ios, что мы не будем синхронизироваться с stdio.
      ios_base::sync_with_stdio(false);
      // Локаль для вывода
      locale cp866(locale(""), new codecvt_cp866<wchar_t, char, mbstate_t>);
      // Сообщаем потоку, что перед выводом необходимо выполнять
      // преобразование
      wcout.imbue(cp866);
    
      // Локаль с правильным ctype
      locale cyrr(locale(""), new unicyr_ctype);
      wsregex_compiler xpr_compiler;
      xpr_compiler.imbue(cyrr);
      wsregex xpr = xpr_compiler.compile(L"МИР", regex_constants::icase);
      wsmatch match;
      if(regex_search(wstring(wstr), match, xpr))
        wcout << L"icase сработал" << endl;
      else
        wcout << L"icase не сработал" << endl;
    
      return 0;
    }
    


    Фасет codecvt для преобразования кириллицы из cp1251 в ucs-2 и обратно

    #include <locale>
    #include <map>
    
    /**@brief Фасет codecvt для преобразования символов кириллицы из cp1251 
     * в UCS-2 и обратно
     *
     * Фасет хорошо описан в книге Страуструпа (3-е специальное издание -
     * приложение). Темным пятном является лишь третий параметр шаблона -
     * состояние. Дело в том, что фасет codecvt спроектирован для работы с
     * потоками байт. Символ может представлять собой последовательность из
     * нескольких байт. В случае, если мы получили только часть байт,
     * составляющих символ, мы не можем преобразовать его. Необходимые байты
     * могут быть получены в следующий вызов функций преобразования. State
     * таким образом выступает в качестве объекта, в который мы сохраняем
     * информацию о текущем неполном преобразовании для того, чтоб ее можно 
     * было бы сообщить следующему вызову функции преобразования. */
    template<class I, class E, class State>
    class codecvt_cp1251 : public std::codecvt<I, E, State>
    {
      public:
    
        // результат выполнения операции преобразования
        typedef typename std::codecvt_base::result result;
        const result ok,      // преобразовано
                     partial, // преобразовано частично (см описание State)
                     error,   // произошла ошибка
                     noconv;  // не преобразовано
    
        explicit codecvt_cp1251(size_t r=0)
          : std::codecvt<I, E, State>(r),
            ok(std::codecvt_base::ok),
            partial(std::codecvt_base::partial),
            error(std::codecvt_base::error),
            noconv(std::codecvt_base::noconv)
        {
          // расширенные символы кириллицы - см стандарт
          in_tab[0xA8] = 0x401;  out_tab[0x401] = 0xA8;
          in_tab[0xB8] = 0x451;  out_tab[0x451] = 0xB8;
          // ... остальные коды
        }
    
        ~codecvt_cp1251()
        {
        }
    
    protected:
      /**@brief Выполняет преобразование из внешней кодировки во внутреннюю 
       * строки from-from_end, записывая результат в строку in-in_end.*/
      virtual result do_in(State&,
                   const E* from, const E* from_end, const E* &from_next,
                   I* to, I* to_end, I* &to_next)
        const
      {
        while(from != from_end)
        {
          if(to == to_end)
          {
            from_next = ++from;
            to_next = to;
            return partial;
          }
          // ASCII
          if(0 <= *from && *from <= 0x7F)
          {
            *to = static_cast<I>(*from);
          }
          else if(0xC0 <= static_cast<unsigned char>(*from)
                       && static_cast<unsigned char>(*from) <=0xFF)
          {
            *to = static_cast<I>(static_cast<unsigned char>(*from) + 0x350);
          }
          else
          {
            typename std::map<E, I>::const_iterator s;
            s = in_tab.lower_bound(*from);
            if(s == in_tab.end())
            {
              // ранее мы удостоверились, что from и next не последние
              from_next = ++from;
              to_next = ++to;
              return error;
            }
            *to = s->second;
          }
          ++to;
          ++from;
        }
        from_next = from_end;
        to_next = to;
        return ok;
      }
    
      /**@brief Если преобразования не нужны, возвращает true*/
      virtual int do_encoding() const throw()
      {
        return 1;
      }
    
      /**@brief Если преобразования не нужны, возвращает true*/
      virtual bool do_always_noconv() const throw()
      {
        return false;
      }
      /* не будем усложнять пример
      virtual result do_out(State&,
                    const I* from, const I* from_end, const I* &from_next,
                    E* to, E* to_end, E* &to_next);
      virtual int do_length(State& s, const E* from, const E* from_end, size_t max) const;
      virtual int do_max_length() const throw();
      */
    private:
      // для хранения расширенной части символов кириллицы
      std::map<E, I> in_tab;
      std::map<I, E> out_tab;
    };
    


    Примечания


    Я не стал захламлять статью лишним кодом — фасет codecvt для cp866 реализуется аналогично, а о ctype я рассказывал ранее, но если кому-то нужен рабочий пример, эти фасеты можно взять на github — git://github.com/hoxnox/cyrillic-facets.git.
    И прошу прощения за отсутствие номеров строк — чтобы НЛО «проглотило» топик пришлось сокращать highight кода.

    Подробней о UNICODE модно почитать тут
    Подробней о фасетах — в книге Страуструпа «Язык программирования C++», третье специальное издание, приложение
    Подробней о boost::xpressive тут

    UPD 20150813:
    Хабрапользователь Cyapa провел расследование и выяснил, что фасет codecvt работает только с потоками, работающими с bisic_filebuf. Согласно стандарту (п. 27.9.1.1, кроме того это отмечено у Страуструпа) реализации не обязаны вызывать методы фасета codecvt для других буферов — в частности basic_stringbuf. Таким образом если вы создали локаль со своим фасетом codecvt и надеетесь, что std::stringstream, которому назначена эта локаль (с помощью imbue) будет дергать этот фасет, вы ошибаетесь.
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 61
    • +1
      Вы за русскоязычные комментарии?
      • +6
        Вообще, конечно же за англоязычные.
        В редких случаях приходится писать русскоязычные (для отдельных «личностей»). В данном случае — это лишь пример к статье на русском языке.
        • 0
          Спасибо :-) Интересный материал.
          • +1
            Поясните, а в каких случаях вам вообще может потребоваться указывать юникодные константы в тексте программы?

            • 0
              Частенько.
              Совсем недавно писали разборщик полуструктурированных файлов на русском языке (cp1251, cp866, utf-8) для наполнения по ним базы данных. Самым простым вариантом реализации были регулярки. В этом проекте меня и удивила команда своими подходами к работе с различными кодировками и обращение с xpressive.
              • 0
                Логично, спасибо. Правда, тут же возникает второй логичный вопрос: чем обусловлено решение такой задачи на C++?
                • 0
                  Количество файлов ~300 000. Ежедневно поступает ~100-200. Да и команда больше знакома с крестами…
              • 0
                Разбор математических или химических формул.
            • +1
              По-моему, в русскоязычных комментариях нет ничего плохого. Если, конечно, это не иностранная айти-фирма с англоязычными программистами.
              • +2
                а как же open source
                • 0
                  С доводом согласен. Зачастую — это достаточный аргумент.
                • 0
                  Никогда не знаешь, что случится с кодом через несколько лет. Вдруг вы этот код отдадите поддерживать и/или развивать на аутсорс в Индию или Китай?
                  • 0
                    В Индию или Китай? Я — точно нет. Такова специфика. У других, допускаю, иначе.
                • 0
                  что плохого в русскоязычных комментариях?
                  • 0
                    Выше уже рассказали :-) Open source.
                    От себя могу добавить, что это позволяет избежать лишних разговоров с людьми, которым в этом коде явно не стоит копаться.
                    Да и в конце концов, зачем переключать раскладку? Это уменьшает скорость печати :-)
                • –1
                  Спасибо, ваша позиция ясна.
                  • +4
                    UTF-8 с BOM — это нелепая выдумка Microsoft, потому что Byte Order Mark не имеет никакого смысла для UTF8.
                    MS Visual Studio прекрасно отображает UTF8 файлы без этой метки, а использовать строчные литералы в не ASCII кодировке я считаю дурной практикой.
                    • 0
                      Отображает — да, отлично. А попробуйте скомпилировать вот это в файле UTF-8 без BOM и с BOM:
                      1.  
                      2. std::wstring wstr=L"АБВГ";
                      3. std::cout << (wstr[0] == 0x0410 ? "ok" : "fail" );
                      4.  


                      >а использовать строчные литералы в не ASCII кодировке я считаю дурной практикой
                      Я уже говорил, что порой это необходимость. Если использовать юникодные константы получаются таки крокодилы в коде, в которых потом очень сложно разобраться. Последний пример — анализ текстов.
                      • +5
                        GCC 4.4.3 совершенно пофигу, есть в UTF-8-файле BOM или нет. В обоих случаях вывод совершенно идентичный, такой:
                        merlin@merlin-hpmini ~/asdf $ cat test.cpp
                        #include main() {

                        std::wstring wstr=L«АБВГ»;
                        std::cout << (wstr[0] == 0x0410? «ok»: «fail» );

                        }
                        merlin@merlin-hpmini ~/asdf $ g++ test.cpp -o test
                        merlin@merlin-hpmini ~/asdf $ ./test
                        ok
                        Вообще, BOM имеет смысл для всех юникодных кодировок, кроме UTF-8. В этой кодировке уже жестко зафиксирован порядок байтов, он не зависит ни от каких маркеров и соответственно в них нет никакого смысла.

                        Более того, отдельным программам становится плохо от этих маркеров. В примере выше cat и g++ просто не заметили его, как будто его и нет.
                        • 0
                          (простите, блокквоту забыл закрыть)
                          • 0
                            Добавлю, что просто скомпилированные файлы отличаются в размере на два байта. Если выполнить strip test для каждого варианта, то они становятся идентичными.
                            • 0
                              gcc — да, пофигу
                              Речь шла о msvc. И тут дело не в том, что он порядок байт путает…
                              • 0
                                Он что же, саму кодировку по BOMу опознаёт?
                                • 0
                                  Ну почти…
                                  Википедия пишет:
                                  While Unicode standard allows BOM in UTF-8 [2], it does not require or recommend it[3]. Byte order has no meaning in UTF-8[4] so a BOM only serves to identify a text stream or file as UTF-8 or that it was converted from another format that has a BOM.

                                  BOM помогает понять msvc, что файл в UTF-8 и правильно интерпретировать L«АБВГ» как 4 символа wchar_t, а не 8 символов char.
                              • –1
                                кажись gcc работает в текущей локали, то есть вы написали L«АБВГ» в текущей локали, gcc и распознал это в текущей локали.
                            • 0
                              Попробуйте написать автоматическое определения кодировки. Для определения файлов в UTF8 без BOM требуется немало кода(к примеру можно глянуть код Notepad++), мне кажется всетаки смысл в нем есть.
                              • 0
                                толку? все равно код определения кодировки придется писать, потому что не все файлы имеют bom
                                • 0
                                  Хотя бы если BOM есть очень быстро кодировка определится. Надо было сделать BOM обязательным, тогда бы не у кого проблем не было.
                            • +1
                              setlocale(LC_ALL, "");

                              mbstowcs
                              wcstombs

                              всё.
                              • 0
                                Это если текущая локаль пользователя на платформе обеспечивает нужное преобразование (нужные фасеты имеются в локале пользователя). В противном случае получите белиберду или исключение.
                              • +16
                                Я считаю, что нет достаточно жестокого наказания для тех людей, которые сегодня начинают писать новый софт, используя cp125*
                                • +1
                                  К сожалению, до сих пор используются эмуляторы терминалов и файловые системы, использующие национальные однобайтовые кодировки по-умолчанию.
                                  • 0
                                    С терминалами можно бороться перекодированием, а что за файловые системы вы имели в виду?
                                    • 0
                                      FAT, например. Знаете, такая файлуха, в которую обычно форматируют флешки.
                                      • 0
                                        FAT не умеет юникодные имена только в случае 8.3

                                        Но все винды начиная с 95 OSR 2 и NT 4.0 умеют длинные имена, которые хранятся именно в юникоде… Так что поясните, о каких именно проблемах с юникодом в фате речь?
                                        • 0
                                          Не знаю насчёт виндов, но у PSP мне флешку отформатила так, что монтировать её надо, указывая кодировкой cp1251. Гнум c KDE сами с определением справляется, но факт остаётся фактом.
                                          • 0
                                            Вот уж не знаю, где вы такой PSP взяли, у меня нормально форматирует. И на FAT никогда не использовалась cp1251. может в этом проблема?
                                            • 0
                                              Не знаю, может, прошивка такая, но при монтировании без параметров вижу кракозябры
                                              • +1
                                                Если у вас локаль cp1251, то надо указать iocharset=cp1251, mount сам не догадается.
                                  • +2
                                    Если они сидят под виндой, то можно просто выставить им дефолтной кодировкой для неюникодных программ, скажем, иврит или грузинский (любой язык с алфавитом не из латиницы и не из кириллицы) и отобрать права на изменение настроек локали. Через пару дней они станут true-юникодерами :)

                                    Ох и натерпелся я от таких горе-программистов, но зато перешёл на правильный софт, который не страдает старыми кодировками.
                                  • +2
                                    > Если кто не знает, в vim BOM можно добавить к файлу с помощью команды «set bomb».
                                    Сильно улыбнуло :)
                                    • +2
                                      Э-э-э,

                                      unicyr_ctype.hpp, line 189

                                      std::map<mask, wchar_t> masks;

                                      а не наоборот ли?

                                      p.s. пардон, если что, очень спать хочется :)

                                      • 0
                                        Да, похоже вы правы, спасибо — закоммитил.
                                        А спать действительно хочется =)
                                      • +3
                                        Спасибо за статью.
                                        Теперь я лучше понимаю, что дала миру языков программирования Java.
                                        • 0
                                          Простите, вплотную с Java не работал, но там чтение файлов в различных кодировках или получение данных из какого-либо потока в разных кодировках делается ява-машиной?
                                          • 0
                                            В принципе, если хорошо почитать доки перед тем, как начинать работу с потоками, то все ОК.
                                            Но вывод в виндовую консоль по-прежнему — нетривиальная задача. Хотя в JSE6 класс Console сииильно облегчил задачу.
                                            • 0
                                              Тогда я не понял фразу товарища OnoIt… Что такого Java дала миру языков программирования в обсуждаемой области?!
                                              • 0
                                                ИМХО для меня главное — там есть только ОДИН ТИП СТРОКИ.
                                                И все конверсии данных «извне» проходят довольно незаметно, если, конечно, использовать предназначенные для этого классы (это к заметке об чтении документации, а то такие велосипеды встечал — о-го-го! Гослинг плачет кровавыми слезами)
                                            • 0
                                              Надо задавать кодировку для ввода-вывода, ява-машина прозрачно преобразует из внешней кодировки в UNICODE и обратно ( посмотрите InputStreamReader/OutputStreamWriter объекты )

                                              Тип String имеет UNICODE кодировку по умолчанию, можно создать строку с другой кодировкой, или преобразовать массив байтов в строку в соответствии с кодировкой.

                                              Отвечу и по другим темам статьи.

                                              При компиляции кодировка исходников также задается ключом компилятора. Имена переменных и пр. можно писать в UNICODE — но с этим надо быть осторожным, мы пишем все на английском, а комментарии можно и на русском.

                                              Regex объекты знают про UNICODE.

                                              Locale объект позволяет задавать дату, время, числа с разделителями в формате для определенного языка, страны — удобно.

                                              • 0
                                                Ошибся — строки всегда в UNICODE ( «A String represents a string in the UTF-16 format» ), а конструктор строки может принимать байтовый массив и название кодировки.
                                            • +1
                                              Не знаю, как обстоят дела на данный момент, но лет пять назад на всех ява-форумах была ветка (и не одна), как заставить яву понимать кириллицу :) А в русскоязычных книгах по яве была специальная сноска, что, дескать, юникод юникодом, но для поддержки кириллицы есть специальный бубен :)
                                            • +1
                                              более логично перейти на расширенные символы wchar_t и использовать UCS-2.

                                              Если так хочется двухбайтный wchar_t, то лучше использовать UTF-16. UCS-2 устарел.
                                              • 0
                                                Я, похоже, не до конца разобрался в вопросе. Не знаю точно с чем конкретно работают современные компиляторы. Наверное UTF-16…
                                                Из unicode.org:
                                                UTF-16 and UCS-2 are identical for purposes of data exchange. Both are 16-bit, and have exactly the same code unit representation.
                                                • +1
                                                  www.unicode.org/faq/basic_q.html#14
                                                  В обмене данных они одинаковые, в обработке UTF-16 подразумевает, например, поддержку суррогатных пар. В UCS-2 подразумевается, что за пределами BMP юникода нет.
                                                  • –1
                                                    IMHO, современные компиляторы используют UCS2. Потому что он намного удобней в работе, 2 байта всегда ровно один символ.
                                                    Все стандартные функции поддерживающие wchar_t расчитаны на работу сUCS2 строками
                                                    • +3
                                                      >Потому что он намного удобней в работе, 2 байта всегда ровно один символ.
                                                      На самом деле, удобнее всего ASCII. Один байт всегда ровно один символ, да ещё и старший бит можно использовать как угодно.
                                                      >Все стандартные функции поддерживающие wchar_t расчитаны на работу сUCS2 строками
                                                      Не знаю, как в win (которая вроде бы после Win2000 переходила на UTF-16), но в *nix под wchar_t обычно понимают UCS-4. И вообще, рассчитывать на конкретный размер wchar_t — моветон.
                                                      • –2
                                                        Да в gcc и vc отличаются размеры wchar_t, но не в том смысл. Смысл в том что в UCS один символ всегда sizeof(wchar_t) байт и это удобно. думаю даже в gcc стандартные строковые функции рассчитаны что wchar_t* это UCS строка.
                                                        • +2
                                                          >даже в gcc стандартные строковые функции рассчитаны что wchar_t* это UCS строка.
                                                          В gcc все функции рассчитаны на то, что wchar_t* — это UCS-4 строка. А в ней суррогатные пары не нужны. Конечно, даже это не освобождает от комбинирующих и null-width символов.
                                                          >и это удобно
                                                          А я о чём? Удобно использовать char для хранения строк, а с включенным старшим битом — для escape и прочих личных целей. Правда, дикари из одной северной страны после этого возникают с « Е ПРОМЕ ЯЕМ БУКВУ    А ПОГА ЫЙ ПРОБЕЛ» и багрепортят о проблемах с буквой «я», но нам какое дело?
                                                      • –1
                                                        msdn.microsoft.com/en-us/library/dd374069(VS.85).aspx
                                                        Можно не гадать. UTF-16. UCS-2 надёжно закопан, туда ему и дорога.
                                                        • –1
                                                          Там не то написано, там перечисленно какие компоненты поддерживают UTF16 в Windows.

                                                          Посмотрите к примеру реализацию std::wstring или wcslen.
                                                  • 0
                                                    Потрясение устоями! ostreambuf_iterator можно не разыменовывать, у него перегружен оператор =!

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