Pull to refresh

Не забываем о языковых и культурных особенностях

Reading time 6 min
Views 24K
Рано или поздно все сталкиваются с проблемами связанными с языковым и культурным разнообразием при написании программ. Я был сильно удивлен узнав, что часть моих знакомых, пишущих на C++, решают эти проблемы своими велосипедами. Для тех, кто еще не знает что такое std::locale я хотел бы кратко на примере показать как c ним работать и что бывает, если о нем забыть…

std::locale (локализация) — это объект, позволяющий учитывать культурные и языковые особенности пользователей. По сути, это контейнер специальных классов — фасетов, к которому обращается программа, если ей необходимо произвести действия, зависящие от естественного языка. Программа поручает подобные действия фасетам локализации. В локализацию могут быть добавлены любые пользовательские фасеты. Но наибольший интерес представляют стандартные, так как они реализованы в любой локализации и могут быть перманентно или на время подменены:
  • collate (сравнение строк)
  • numeric (ввод/вывод чисел)
  • monetary (ввод/вывод денег)
  • time (ввод/вывод времени)
  • ctype (классификация символов)
  • messages (выборка сообщений)

В реальности мы постоянно используем фасеты, даже не подозревая этого. Стандартная библиотека шаблонов использует лакализацию для ввода/вывода. boost::regex для преобразования регистра символов и т.д. Локализация задается платформой. Пользователям *nix систем знакомы такие строки как «ru_RU:UTF-8», «en_US.UTF-8» — это названия локализаций в платформе. Программа использует пользовательскую локализацию. Если пользователем локализация не задана, используется «классическая».

Пример использования локализации и переопределения фасета


Рассмотрим пример, в котором попробуем технику подмены стандартного фасета локализации. Обычно рассматривают потоковый ввод/вывод, но я хотел бы заострить внимание на том, что может происходить, если писать код, зависимый от локализации, не зная что это такое. Попробуем использование локалей с распространенной библиотекой boost::xpressive (можно использовать и boost::regex, но тем кто первый раз слышит про xpressive, будет полезно о нем почитать):

  1.  
  2. #include <boost/xpressive/xpressive.hpp>
  3. #include <iostream>
  4.  
  5. using namespace std;
  6. using namespace boost::xpressive;
  7.  
  8. int main(int argc, char *argv[])
  9. {
  10.   sregex xpr = sregex::compile("мир", regex_constants::icase);
  11.   smatch match;
  12.   string str("ПРИВЕТ МИР!");
  13.   if(regex_search(str, match, xpr))
  14.     cout << "icase ok" << endl;
  15.   else
  16.     cout << "icase fail" << endl;
  17.   return 0;
  18. }
  19.  


Некоторых удивит, что выдача программы сильно зависит от платформы. Более того, на одной платформе программа может выдавать различные результаты. Все дело в locale. Если предположить, что кодировка файла примера — windows-1251, то результат «icase fail» можно достичь на платформе, в которой пользовательская локаль имеет кодировку, отличную от cp1251. Самый распространенный пример такой платформы — mingw (скачанный как бинарник с sourceforge) + Windows. В таком случае, алгоритмы boost::xpressive просто не знают какие символы в расширенной части кодовой таблицы cp-1251 являются буквами. И виноват в этом фасет ctype классической локализации. Сообщив правильный фасет ctype локализации, с которой работает xpressive, мы добъемся нужного результата. В простейшем случае, если в системе установлена нужная локализация, нам достаточно сделать ее глобальной

  1.  
  2. //устанавливаем глобальную локализацию
  3. std::locale cp1251_locale("ru_RU.CP1251");
  4. std::locale::global(cp1251_locale);
  5.  

либо сообщить о ней компилятору regex-ов
  1.  
  2. std::locale cp1251_locale("ru_RU.CP1251");
  3. sregex_compiler compiler;
  4. // сообщаем компилятору regex-ов какой локализацией пользоваться
  5. compiler.imbue(cp1251_locale);
  6. sregex xpr = compiler.compile("мир", regex_constants::icase);
  7.  


Все бы ничего, но на платформе, где не поддерживается локализация ru_RU:CP1251 наш код выкинет исключение. В лучшем случае, неправильно указано имя, в худшем — нужной локализации нет в системе. Решим эту проблему реализацией собственного фасета ctype (именно он объяснит xpressive какие символы являются буквами и каким образом меняется регистр).

Простейший пример реализации фасета ctype и примера, для кодировки CP1251:

  1.  
  2. #include <boost/xpressive/xpressive.hpp>
  3. #include <iostream>
  4.  
  5. using namespace std;
  6. using namespace boost::xpressive;
  7.  
  8. /**@brief Очень упрощенный пример фасета ctype для корректной работы с
  9.  * кодировкой Cp1251*/
  10. class ctype_cp1251 : public ctype<char>
  11. {
  12.   public:
  13.    
  14.     /**@breif mask в ctype_base - это перечисление всех возможных типов
  15.      * символов - alpha, digit, ...*/
  16.     typedef typename ctype<char>::ctype_base::mask mask;
  17.    
  18.     // для краткости переобозначим константы
  19.     enum{
  20.       alpha = ctype<char>::alpha,
  21.       lower = ctype<char>::lower,
  22.       punct = ctype<char>::punct
  23.       // другие маски
  24.     };
  25.    
  26.     /**@brief Основной конструктор. r - характеризует область жизни
  27.      * фасета. Подробней см. в книге Страуструпа.*/
  28.     ctype_cp1251(size_t r = 0)
  29.     {
  30.       // инициализируем таблицу масок. Индекс - отрицательная часть char.
  31.       // То есть ext_tab[1] - маска для символа char(-1) - 'я'
  32.       ext_tab[0] = 0;
  33.       for(size_t i = 1; i <=32; ++i)
  34.         ext_tab[i] = alpha | lower;
  35.       for(size_t i = 33; i <= 64; ++i)
  36.         ext_tab[i] = alpha | upper;
  37.       // ... остальные символы в данном примере неинтересны
  38.       for(size_t i = 65; i <= 128; ++i)
  39.         ext_tab[i] = punct;
  40.     }
  41.  
  42.     ~ctype_cp1251()
  43.     { }
  44.  
  45.   protected:
  46.    
  47.     /**@brief Отвечает на вопрос соответствует ли символ c маске m*/
  48.     virtual bool is(mask m, char c) const
  49.     {
  50.       if(0 <= c && c <= 127)
  51.         return ctype<char>::is(m, c);
  52.       else if(-128 <= c && c < 0)
  53.         return ext_tab[static_cast<size_t>(c*-1)] & m;
  54.     }
  55.    
  56.     /**@brief Преобразует символ c в верхний регистр*/
  57.     virtual char do_toupper(char c) const
  58.     {
  59.       if(0 <= c && c <=127)
  60.         return ctype<char>::do_toupper(c);
  61.       else if(is(lower, c))
  62.         return c - 32;
  63.       return c;
  64.     }
  65.    
  66.     /**@brief Преобразует символ c в нижний регистр*/
  67.     virtual char do_tolower(char c) const
  68.     {
  69.       if(0 <= c && c <=127)
  70.         return ctype<char>::do_tolower(c);
  71.       else if(is(upper, c))
  72.         return c + 32;
  73.       return c;
  74.     }
  75.    
  76.     // чтобы не усложнять пример, не будем переопределять остальные
  77.     // виртуальные функции
  78.  
  79.   private:
  80.     // запрет на копирование
  81.     ctype_cp1251(const ctype_cp1251&);
  82.     const ctype_cp1251& operator=(const ctype_cp1251&);
  83.     mask ext_tab[129]; //@< маски расширенной части кодовой таблицы CP1251
  84. };
  85.  
  86. int main(int argc, char *argv[])
  87. {
  88.   // создаем экземпляр фасета
  89.   ctype<char> *ctype_cp1251_facet = new ctype_cp1251();
  90.  
  91.   // Создаем новую локализацию на основе текущей, использующей
  92.   // определенный выше фасет. Можно определить глобальную
  93.   // локализацию с описанным фасетом, тогда все классы и
  94.   // функции, будут использовать именно ее.
  95.   locale cp1251_locale(locale(""), ctype_cp1251_facet);
  96.  
  97.   // создадим компилятор regex-ов с конкретной локализацией
  98.   sregex_compiler compiler;
  99.   compiler.imbue(cp1251_locale);
  100.  
  101.   sregex xpr = compiler.compile("мир", regex_constants::icase);
  102.   smatch match;
  103.   string str("ПРИВЕТ МИР!");
  104.   if(regex_search(str, match, xpr))
  105.     cout << "icase ok" << endl;
  106.   else
  107.     cout << "icase fail" << endl;
  108.   return 0;
  109. }
  110.  


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

Полное описание класса std::local и техники использования фасетов можно найти в 3-ем специальном издании книги Бьерна Страуструпа «Язык программирования C++», в приложении. Для уточнения структуры фасетов, можно воспользоваться любым руководством по STL. Например тут.

Задача преобразования кодировок решается реализацией фасета codecvt. Если будет интересно, расскажу о ней в следующей статье.

______________________
Текст подготовлен в Редакторе Блогов от © SoftCoder.ru
Tags:
Hubs:
+64
Comments 27
Comments Comments 27

Articles