
std::locale (локализация) — это объект, позволяющий учитывать культурные и языковые особенности пользователей. По сути, это контейнер специальных классов — фасетов, к которому обращается программа, если ей необходимо произвести действия, зависящие от естественного языка. Программа поручает подобные действия фасетам локализации. В локализацию могут быть добавлены любые пользовательские фасеты. Но наибольший интерес представляют стандартные, так как они реализованы в любой локализации и могут быть перманентно или на время подменены:
- collate (сравнение строк)
- numeric (ввод/вывод чисел)
- monetary (ввод/вывод денег)
- time (ввод/вывод времени)
- ctype (классификация символов)
- messages (выборка сообщений)
В реальности мы постоянно используем фасеты, даже не подозревая этого. Стандартная библиотека шаблонов использует лакализацию для ввода/вывода. boost::regex для преобразования регистра символов и т.д. Локализация задается платформой. Пользователям *nix систем знакомы такие строки как «ru_RU:UTF-8», «en_US.UTF-8» — это названия локализаций в платформе. Программа использует пользовательскую локализацию. Если пользователем локализация не задана, используется «классическая».
Пример использования локализации и переопределения фасета
Рассмотрим пример, в котором попробуем технику подмены стандартного фасета локализации. Обычно рассматривают потоковый ввод/вывод, но я хотел бы заострить внимание на том, что может происходить, если писать код, зависимый от локализации, не зная что это такое. Попробуем использование локалей с распространенной библиотекой boost::xpressive (можно использовать и boost::regex, но тем кто первый раз слышит про xpressive, будет полезно о нем почитать):
-
- #include <boost/xpressive/xpressive.hpp>
- #include <iostream>
-
- using namespace std;
- using namespace boost::xpressive;
-
- int main(int argc, char *argv[])
- {
- sregex xpr = sregex::compile("мир", regex_constants::icase);
- smatch match;
- string str("ПРИВЕТ МИР!");
- if(regex_search(str, match, xpr))
- cout << "icase ok" << endl;
- else
- cout << "icase fail" << endl;
- return 0;
- }
-
Некоторых удивит, что выдача программы сильно зависит от платформы. Более того, на одной платформе программа может выдавать различные результаты. Все дело в locale. Если предположить, что кодировка файла примера — windows-1251, то результат «icase fail» можно достичь на платформе, в которой пользовательская локаль имеет кодировку, отличную от cp1251. Самый распространенный пример такой платформы — mingw (скачанный как бинарник с sourceforge) + Windows. В таком случае, алгоритмы boost::xpressive просто не знают какие символы в расширенной части кодовой таблицы cp-1251 являются буквами. И виноват в этом фасет ctype классической локализации. Сообщив правильный фасет ctype локализации, с которой работает xpressive, мы добъемся нужного результата. В простейшем случае, если в системе установлена нужная локализация, нам достаточно сделать ее глобальной
-
- //устанавливаем глобальную локализацию
- std::locale cp1251_locale("ru_RU.CP1251");
- std::locale::global(cp1251_locale);
-
либо сообщить о ней компилятору regex-ов
-
- std::locale cp1251_locale("ru_RU.CP1251");
- sregex_compiler compiler;
- // сообщаем компилятору regex-ов какой локализацией пользоваться
- compiler.imbue(cp1251_locale);
- sregex xpr = compiler.compile("мир", regex_constants::icase);
-
Все бы ничего, но на платформе, где не поддерживается локализация ru_RU:CP1251 наш код выкинет исключение. В лучшем случае, неправильно указано имя, в худшем — нужной локализации нет в системе. Решим эту проблему реализацией собственного фасета ctype (именно он объяснит xpressive какие символы являются буквами и каким образом меняется регистр).
Простейший пример реализации фасета ctype и примера, для кодировки CP1251:
-
- #include <boost/xpressive/xpressive.hpp>
- #include <iostream>
-
- using namespace std;
- using namespace boost::xpressive;
-
- /**@brief Очень упрощенный пример фасета ctype для корректной работы с
- * кодировкой Cp1251*/
- class ctype_cp1251 : public ctype<char>
- {
- public:
-
- /**@breif mask в ctype_base - это перечисление всех возможных типов
- * символов - alpha, digit, ...*/
- typedef typename ctype<char>::ctype_base::mask mask;
-
- // для краткости переобозначим константы
- enum{
- alpha = ctype<char>::alpha,
- lower = ctype<char>::lower,
- punct = ctype<char>::punct
- // другие маски
- };
-
- /**@brief Основной конструктор. r - характеризует область жизни
- * фасета. Подробней см. в книге Страуструпа.*/
- ctype_cp1251(size_t r = 0)
- {
- // инициализируем таблицу масок. Индекс - отрицательная часть char.
- // То есть ext_tab[1] - маска для символа char(-1) - 'я'
- ext_tab[0] = 0;
- for(size_t i = 1; i <=32; ++i)
- ext_tab[i] = alpha | lower;
- for(size_t i = 33; i <= 64; ++i)
- ext_tab[i] = alpha | upper;
- // ... остальные символы в данном примере неинтересны
- for(size_t i = 65; i <= 128; ++i)
- ext_tab[i] = punct;
- }
-
- ~ctype_cp1251()
- { }
-
- protected:
-
- /**@brief Отвечает на вопрос соответствует ли символ c маске m*/
- virtual bool is(mask m, char c) const
- {
- if(0 <= c && c <= 127)
- return ctype<char>::is(m, c);
- else if(-128 <= c && c < 0)
- return ext_tab[static_cast<size_t>(c*-1)] & m;
- }
-
- /**@brief Преобразует символ c в верхний регистр*/
- virtual char do_toupper(char c) const
- {
- if(0 <= c && c <=127)
- return ctype<char>::do_toupper(c);
- else if(is(lower, c))
- return c - 32;
- return c;
- }
-
- /**@brief Преобразует символ c в нижний регистр*/
- virtual char do_tolower(char c) const
- {
- if(0 <= c && c <=127)
- return ctype<char>::do_tolower(c);
- else if(is(upper, c))
- return c + 32;
- return c;
- }
-
- // чтобы не усложнять пример, не будем переопределять остальные
- // виртуальные функции
-
- private:
- // запрет на копирование
- ctype_cp1251(const ctype_cp1251&);
- const ctype_cp1251& operator=(const ctype_cp1251&);
- mask ext_tab[129]; //@< маски расширенной части кодовой таблицы CP1251
- };
-
- int main(int argc, char *argv[])
- {
- // создаем экземпляр фасета
- ctype<char> *ctype_cp1251_facet = new ctype_cp1251();
-
- // Создаем новую локализацию на основе текущей, использующей
- // определенный выше фасет. Можно определить глобальную
- // локализацию с описанным фасетом, тогда все классы и
- // функции, будут использовать именно ее.
- locale cp1251_locale(locale(""), ctype_cp1251_facet);
-
- // создадим компилятор regex-ов с конкретной локализацией
- sregex_compiler compiler;
- compiler.imbue(cp1251_locale);
-
- sregex xpr = compiler.compile("мир", regex_constants::icase);
- smatch match;
- string str("ПРИВЕТ МИР!");
- if(regex_search(str, match, xpr))
- cout << "icase ok" << endl;
- else
- cout << "icase fail" << endl;
- return 0;
- }
-
Теперь результат программы не будет зависеть от конкретной платформы. Переопределяя стандартные фасеты или добавляя новые, можно управлять поведением алгоритма/программы в зависимости от культурных и языковых особенностей пользователей.
Полное описание класса std::local и техники использования фасетов можно найти в 3-ем специальном издании книги Бьерна Страуструпа «Язык программирования C++», в приложении. Для уточнения структуры фасетов, можно воспользоваться любым руководством по STL. Например тут.
Задача преобразования кодировок решается реализацией фасета codecvt. Если будет интересно, расскажу о ней в следующей статье.
______________________