7 ноября 2010 в 21:18

Кодировки

C++*

Всем рано или поздно приходится работать с различными кодировками. Заметив в коде своей команды различные, порой странные, подходы к решению этих проблем, пришлось провести разъяснительную беседу. Ниже поделюсь своим видением правильной работы с не-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) будет дергать этот фасет, вы ошибаетесь.
@hoxnox
карма
54,2
рейтинг 0,0
Похожие публикации
Самое читаемое Разработка

Комментарии (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 можно не разыменовывать, у него перегружен оператор =!

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