Сэр Серж aka Sir Serge (Сергей Лебедев) - official site
Статьи и заметкиРасчетыСтихиПрозаО сайте

Национальный вопрос в С/С++

Язык программирования C++ является своеобразным фетишем, привлекающим к себе массу народа для "профессионального" программирования. На определенных специальностях в ВУЗах обучают в том числе и ему тоже, что, казалось бы, создаёт весомые предпосылки для того, чтобы по этому языку была масса доступной и грамотной информации, прилагаемой к практической деятельности.

Однако, на деле, несмотря на, количество литературы, (скажем так) - существенно превышающее по числу экземпляров и изданий объем информации по любым другим языкам программирования, истинных жемчужин среди всей этой массы немного. Более того, это в основном переводная американская литература, а тамошние писатели не обеспокаивают себя такими идиотскими вопросами, как вывести на экран или прочитать из файла национальный мусор, не являющийся тру-американскими символами ASCII. Ну и, для разжигания флейма, ввод/вывод в C отделен от непосредственного языка, не так ли? Поэтому что? Правильно - при решении национальных проблем появляются разного рода костыли, порою бросающиеся в глаза или даже составляющие основу композиции, как на некоторых картинах Сальвадора Дали.

Начнем с малого. Итак, "Hello world!", как оно есть и как оно встречается во всех учебниках.

#include <stdio.h>

int main(int argc, char **argv)
{
   printf("Hello, world!\n");
   return 0;
}
Откроем учебник. И вот он, код. Казалось бы, тот же самый:
#include <stdio.h>

int main(int argc, char **argv)
{
   printf("А вот русская строка!\n");
   return 0;
}

Всё правильно? Синтаксис соблюден? Ага.

Правила языка не нарушены? Нет.

Однако, приведенная "русская" интерпретация при реализации современных библиотек времени исполнения и операционных систем - грубейший ляп, не имеющий права на жизнеспособность.

То, что "нахаляву" прокатывало в DOS, становится абсолютно неприемлемым сейчас.

А видим мы так называемое "решение проблемы в лоб", не имеющее ничего общего ни с системным подходом, ни с пониманием того, как это вообще может работать. Ну, сделали, ну получилось кое-как - вот и прекрасно. Это из той же серии, что и до сих пор эксплуатирующиеся в некоторых местах русскоязычные базы данных на галимом FoxPro 2.0, требующие для своего функционирования досовского драйвера клавиатуры, подменяющего русские буквы "p" и "Н" на похожие по очертанию латинские.

В чем здесь проблемы?

Ну, например, под линуксом, с использованием компилятора достаточно свежих версий, с текущей локалью utf-8, казалось бы все срабатывает нормально, за одним "но" - спецификатор форматного вывода не в состоянии сосчитать правильную ширину полей для строковых данных в utf, что приведет к перекосу внешнего вида выводимой информации. Ну, и вы практически теряете возможность манипулировать с отдельными символами строк, ибо шансы разрушить utf совершенно неиллюзорны. Под windows работоспособность напрямую зависит от используемого компилятора - MinGW должен вообще отругать за "invalid characters in string constant", VC++ скорее всего проглотит и может быть даже сработает относительно корректно, естественно, если текст набран в cp866, раритетным компиляторам от Борландов - вообще пофиг, что у исходника в строках - это вообще "вещь в себе", собственный внутренний мир, и свои, неповторимые, глюки.

Хорошо, а как же тогда должно быть? Авторы некоторых немногих учебников по программированию на Visual C++, где эта тема затронута, утверждают, что должно быть что то подобное:

#define _UNICODE
#define UNICODE
#include "StdAfx.h"
#include <stdio.h>

int _tmain(int argc, _TCHAR *argv[])
{
   _tprintf(_T("А вот русская строка!\n"));
   return 0;
}

То есть, в зависимости от пожеланий трудового народа, либо определяем парочку UNICODE, либо не определяем (прелесть в том, что в при создании проекта в VS трансфер этих определений вбивается в командную строку компилятора тихо и незаметно - видимо, чтобы было позапутаннее) - и программа якобы автоматически (!!!) работает либо с уникодными wide строками, либо - с обычными... гм... не работает. Дело в том, что printf/fprintf и прочее из стандартных библиотек могут нормально вывести только символы, совпадающие по кодировке с действующей локалью операционной системы. А в Windows русского типа, или с полной поддержкой русского языка, ели вы не забыли, присутствуют две корректных и несовместимых локали - cp866 - для текстовой консоли, и Basic Russian, cp1251 - для всего остального. Причем, редактор Visual Studio работает исключительно в cp1251 и догадайтесь, что будет выведено на текстовую консоль в результате.

Более того, если вы и определили _UNICODE, то нормальных русских строк не получите тоже. Все потому, что вы не установили явным образом локаль, в которой работает ваша программа. То есть, должно текст наш преобразовать к следующему:

#define _UNICODE
#define UNICODE
#include "StdAfx.h"
#include <locale.h>

int _tmain(int argc, _TCHAR *argv[])
{
   _tsetlocale(LC_ALL,_T("rus_rus.866"));
   _tprintf(_T("А вот русская строка!\n"));
   _TCHAR s[200];
   _tprintf(_T("Ввести текст:"));
   _getts(s);
   _tprintf(_T("Введено: %ls\n"),s);
   return 0;
}

при этом некоторые несознательные личности уверяют, что сойдет и setlocale(LC_ALL,""). Нет, не сойдет. Потому что программа без зазрения совести опеределит дефолтовую локаль как rus_rus.1251. А в этом случае вывод будет действовать нормально (VC++, видимо, специальная коррекция для тех кто об этом забывает), а вот строки, введенные через _getts/getws операции будут то ли в кодировке 1251, то ли вообще испорчены.

Итак, с одной стороны, мы как бы постигаем универсализм через макросы - компилятор сам найдет и заменит на нужные определения функций и типов, в зависимости от того, нужен нам уникод или нет. С другой стороны - как ни крути - нормального отображения русских букв без использования уникодных строк и соотвествующих функций все равно не достигнуть, так для чего тогда нужны все извращения? Макросы, кстати, определены в файле tchar.h, который есть в VC++, есть и в MinGW, зато отсутствует в его прототипе - gcc. То есть, ни портабельности, ни приемлемого результата в итоге. Зато многочисленными подчёркиваниями и лишними скобками заметно ухудшаем читабельность программы и провоцируем ошибки и опечатки.

Избавимся от макросов и получим "истинное лицо программы":

#define _UNICODE
#define UNICODE
#include "StdAfx.h"
#include <locale.h>

int wmain(int argc, wchar_t *argv[])
{
   wsetlocale(LC_ALL,L"rus_rus.866");
   wprintf(L"А вот русская строка!\n");
   wchar_t s[200];
   wprintf(L"Ввести текст:");
   _getws(s);
   wprintf(L"Введено: %ls\n",s);
   return 0;
}

После получения сего благостного результата можно задуматься о Царствии Божием... (..!), но не расслабляться! Дело в том, что кроме прекрасного преобразования российских букв нас настигает следующая подлая идея: функция установки локали прекрасным образом меняет разделитель целой и дробной части в функциях типа scanf, операторах форматного вывода и преобразования, а также строку представления даты по умолчанию )). На что меняет - формально зависит от настроек операционной системы. И тут мы сталкиваемся с тем, что несмотря на постулат какого-то врага: для русских свойственно отделять десятичные разряды строго запятой, большинство файлов с данными и привычки пользователей определят в качестве этого разделителя десятичную точку; при этом в самой операционной системе может быть выставлено что угодно. Метод решения проблемы - на ваше собственное усмотрение. Либо доделывать собственные функции, которые будут принимать числа вне зависимости от того, что за разделитель в них используется (слава б..., у нас еще не принято ставить делители тысяч в числах), либо напрямую менять настройки локали операционной системы соответствующими функциями - шаблончики для всех этих знаков в определяющей структуре есть и могут быть изменены.

Это всё? Куда там! И вообще, то о чем мы столь длительно рассуждали, не имеет формально отношения к C++, - пока обсуждением затронуты лишь стандартные библиотеки и стандартный подход его предшественника - "чистого" C. Кстати, все эти самые операции ввода вывода и работы со строками принадлежат в линуксах "внешней" динамической библиотеке (описываемые далее потоки, кстати, тоже - но уже другой библиотеке), именуемой libc (glibc), и именно там нужно смотреть описания внешнего вида функций и что они делают! Не в описании gcc, не в каких-то фолиантах о правилах программинга, а в первоисточнике. Скачиваем с Проекта GNU описание этой библиотеки и внимательно вчитываемся в текст (он, конечно, только на английском). Прелесть еще и в том, что у gcc, у MinGW и у Visual C++ функции (а особенно функции работы с wide строками) могут не только иметь разные имена, но достаточно распространена разная последовательность и набор этих параметров, а также разное поведение в отношении возвращаемых величин. То есть, при переносе с одного на другое - вам придется с очень большой степенью вероятности править и заново отлаживать программу.

Еще одно замечание. Поскольку у MinGW glibc - тоже динамическая библиотека, которую легко позабыть при распространении готовой программы, то... в windows VC++ получается почти без конкуренции, учитывая полностью переведенную на русский язык достаточно адекватную документацию MSDN.

Намного больший универсализм, и намного меньшее количество ошибок по невнимательности и незнанию при работе со строками, способны нам предоставить истинные средства C++ - а именно, строки и поточный ввод-вывод из библиотеки STL, стандартизация которой, как ни странно, обычно нигде не афишируется, а её средства упоминаются в ничтожно малом количестве трудов и обычно в весьма путанном виде. То есть, если не знаешь о существовании STL - читая книги мэтров - можешь и не узнать никогда.

Нас интересуют буквы национальные, не так ли? А значит, для нас нет никаких string, iostream, cin, cout, cerr и тому подобное, а есть только wstring, wistream, wostream, wcin, wcout, wcerr.

Переписываем наш примерчик.

#define _UNICODE
#define UNICODE
#include "StdAfx.h"
#include <locale.h>
#include <string>
#include <iostream>
#include <iomanip>

using namespace std;

int wmain(int argc, wchar_t *argv[])
{
   wsetlocale(LC_ALL,L"rus_rus.866");
   wcout << L"А вот русская строка!" << endl;
   wstring s;
   wcout << L"Ввести текст:";
   getline(wcin,s);
   wcin.sync();
   wcin.clear();
   wcout << L"Введено: " << s << endl;
   return 0;
}

Обратите внимание, что установка локали осталась С-шного типа. Как ни странно, под windows она действует и на все потоки iostreams, с одной особенностью - буквы вводятся и выводятся, а все остальные установки локали - т.е. форматирование чисел - не затрагиваются. А вот под linux подобной однозначности нет. Кажется, консольный поток вывода при таком определении работает корректно, поток ввода - нет. Поэтому, можно, и наверно даже необходимо задать для каждого потока, на который установки для libc не повлияли, поддерживаемую локаль. Делается это (например для wcin) так:

#include <locale>

....

wcin.imbue(locale("ru_RU.utf-8"));

Для ввода/вывода в собственные файлы через текстовые iostreams - это действие строго обязательно.

Я намеренно выдал в примере "провокационную" локаль utf-8. Не стоит пробовать такое определение в windows (а иногда ох как хотелось бы). В описании библиотек, что на VC, что на MinGW, однозначно сказано, что локалями системы для библиотек ввода-вывода (то есть, для аргументов функции setlocale/wsetlocale) могут быть установлены исключительно локали, имеющие байтовый набор символов и не допускающие отображения одной лексемы несколькими знаками (честно говоря, не могу даже привести примеры из известных европейских языков, где лексема из нескольких знаков, разве что "растянутые" умляуты в немецком - ae ue и ss). То есть, для консоли и i/o штатные библиотеки windows уникод и азиатчину не поддерживают - поэтому даже не рассчитывайте на то, что поставили "ru_RU.utf-8" - получили файл с utf-8. В линуксе можно - здесь нет. И, более того, подозреваю, что и во FreeBSD тоже нельзя; нет там стандартной русской локали с utf-8.

Насчет модификаторов ввода-вывода и о том, как производить форматный вывод чисел и всего остального, распространяться здесь не буду. Призываю лишь быть крайне аккуратными с применением конструкций wcin >> вводимая_переменная; ибо подлость, (а точнее традиции телетайпного ввода, бережно перенесённые в c++), заключены в оставлении в потоке при таких операциях знака перевода строки или вообще хвоста строки, не воспринятого вводом. Не забываем, что при таких операциях "в строку" прочитается не вся введенная строка, а только ее часть до первого разделителя - те же странноватые игрища, что с оператором scanf, приводящие порою к чудесам интерпретации при неявно воздействующих параметрах шаблона ввода. Поэтому настоятельно советую не заниматься трудно отслеживаемыми багами, а (1) сначала вводить всю строку до перевода каретки конструкцией getline(wstream,wstring), а уж затем (2) разбирать введенное, как вам больше понравится - через "широкие" ли аналоги sscanf или wistringstream/wostringstream, или через собственные функции.

Если очень хочется читать напрямую из стандартных потоков числа и строки, то чистить буфер ввода в VC++ можно через fflush(stdin) (несмотря на то, что поток-то - wcin, это обычно срабатывает). Формально, должны срабатывать операторы wcin.sync() - сброс буфера и wcin.clear() - сброс флагов состояния - но они при установленной, или неправильно установленной, локали могут давать некорректные результаты или не действовать вообще.

Что касается самих строк wstring и операций с ними - не ищите в libc отдельных функций, подобных операциям с C-строками и widestrings. Это - объект. Более того, это на самом деле template-класс, элементом которого является wchar_t. Все функции являются методами класса. То есть, надо взять сегмент строки - делаем:

   wstring s1=L"Мой мир мой май";
   wstring s2=s1.substr(4,3);

Или вот так:

   wstring s1=L"Мой мир мой май";
   s1=s1.substr(8);

(ненавязчиво подчеркиваю, что функции не воздействуют на оригинал строки, которому принадлежат, если нужно сохранить результат в той же строке - присваиваем ей результат)

Отдельный случай - операторы поиска в строке, результат которых надо сравнивать отнюдь не с нулём, а со специальной константой wstring::npos, значение которой зависит от конкретной реализации. Это очень важно, и порой доставляет.

И, что интересно, среди многого, доступного для строк, вы не найдете функций преобразования регистра знаков ))

Проблему можете в-общем решить как угодно, а можете воспользоваться "прямым" подходом, если вас не интересуют другие языки кроме русского - применить вот этот небольшой класс:

#include <string>
#include <wchar.h>

using namespace std;

class WSTR
{
  protected:
    wstring lvr;
    wstring upr;
    virtual wstring iCase(wstring s, wstring &from, wstring &to);
    virtual void init(void);
  public:
  WSTR(void);
  wstring UpperCase(wstring s);
  wstring LowerCase(wstring s);
};

    wstring WSTR::iCase(wstring s, wstring &from, wstring &to)
    {
      size_t j=s.length();
      size_t p;
      int i=0;
      for (; i<j; i++) 
      {
         p=from.find(s[i]);
         if (p!=wstring::npos) s[i]=to[p];
      }
      return s;
    }
    
    void WSTR::init(void)
    {
        lvr=L"abcdefghijklmnopqrstuvwxyzабвгдежзийклмнопрстуфхцчшщьъэюяё";
        upr=L"ABCDEFGHIJKLMNOPQRSTUVWXYZАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЬЪЭЮЯЁ";
    }
    
  WSTR::WSTR(void)
  {
     init();
  }
  wstring WSTR::UpperCase(wstring s)
  {
     return iCase(s,lvr,upr);
  }
  wstring WSTR::LowerCase(wstring s)
  {
     return iCase(s,upr,lvr);
  }

(это как бы намек на то, что если чего-то для вас не сделали, то иногда можно гораздо быстрее сделать недостающее самому, чем искать готовое)

Что касается двоичных потоков ввода-вывода, то есть многочисленные упоминания о полном фиаско библиотек iostream в этом отношении. Причем, где-то может работать, а где-то - нет. Потому - лучше не использовать вообще.

Какая либо надежда на "стандартность" по отношению к строкам wide и передаче их в двоичном виде, обречена на трудновыясняемый epic fail. Потому что тип wchar_t - тип непортабельный. И сама величина его, и представление зависит от реализации библиотеки. В Windows он обычно двухбайтный, в gcc в зависимости от версии компилятора и библиотек может быть от четырех до восьми байт.

А раз уж затронута тема о том, сколько памяти занимают символы в wide strings, то стоит не забывать о том, насколько ваш текст будет объемнее при обработке по отношению к оригинальному файлу и хватит ли вам оперативной памяти для двух - восьмикратного увеличения объема. Ну, конечно, если вы этот файл прочитали зараз и потом обрабатываете. Добро пожаловать в национальщину, господа! Цена универсального подхода, как всегда - объемы и дублирование информации :(

Ну и да - widestrings, конечно же, будут обрабатываться дольше обычных байтовых строк.

Теперь вы прониклись искренней ненавистью тру-программеров, не читающих Скрижалей, к национальному вопросу и корнях его замалчивания в руководствах для начинающих?

Цветочки. Только цветочки. Открываем какую-нибудь книгу с заголовком, вещающим, "Программирование в C++ для .NET" и обнаруживаем перелицованное с DOS описание древнего Borland C++ 3.1 и VC шестой версии и краткое описание библиотеки MFC без единого упоминания об управляемом коде и .NET вообще... Не правда ли, обычная картина.

Открываем какой-нибудь форум общения труъ программеров и видим кучу тредов "хрен выводятся проклятые русские буквы" и кучу рекомендаций "набрать текст в правильном редакторе" и "у меня все нормуль, ты лох".

...и начинаем задумываться, является ли показателем профессионализма "знание" элементарщины в отношении этого языка и является ли показателем вообще чего "знание" этого языка в резюме некоторых претендентов... Боюсь, некоторые из них - работающие в софтверных компаниях "профессиональные программисты".

Ну и, напоследок, по поводу пожеланий вьюношеству всегда чтить первоисточники. В отношении C++ это фолиант Бъёрна Строуструпа об C++ как об языке программирования. И существуют его два издания. Первое - года то ли 1990, то ли 1992, где товарищ излагает свое мировоззрение относительно еще не реализованных и не ставших стандартом библиотек потокового и строкового ввода, а потому являющееся неверным источником, имеющим малое отношение к действительности, и издание второе, исправленное, насколько мне не изменяет память, в 1996 году - гораздо менее распространенное. Поэтому, если видите в тексте в примерах для потокового вывода функцию типа format("%12.3f") - источник фтопку! Ибо это не единственный в нём идеализм.

Sir Serge, 2011-12-09, Barnaul RF


Вы можете добавить свои комментарии.

Поскольку у нас тут абсолютная демократия, то комментарий появится на сайте только после того, как он будет одобрен администрацией. Оперативности, однако, не обещаем.

Прошу соблюдать относительную корректность в высказываниях. Заявления типа "Пошел на...", посты, написанные в олбанской лексике и психоанализ личности автора и участников обсуждения в свет не выйдут. Также будут блокированы сообщения, не имеющие никакого отношения к заявленной тематике. Если вы не согласны с приведенным текстом - выскажите своё мнение, но обосновывайте его. Помните, что свою позицию доказываете Вы не мне, а другим читателям. Всячески приветствуются возможные технические поправки и исправления неточностей. Для возможности внесения комментариев в браузере должна быть включена поддержка JavaScript. Реклама и ссылки на сайты, не относящиеся к делу, являются прямым основанием блокировки. Поля "E-mail" и "WWW" обязательными для заполнения не являются, поле E-Mail не публикуется. Если хотите просто что-то написать автору статьи, без публикации на сайте - воспользуйтесь специальной формой под пунктом меню "О сайте". Администрация оставляет за собой право публиковать или не публиковать адреса, введенные в поле www, а также при необходимости редактировать текст вашего сообщения. Ответы на ваши сообщения по введенному вами E-mail автоматически сайтом не высылаются. Да, теги PHPBB и HTML не действуют, так что не старайтесь их вводить.

Copyright © 2003-2018 by Sir Serge