Применение псевдошаблонов как альтернатива шаблонов С++

Mykola Demko | 22 февраля, 2011


Введение

На форуме mql5.com неоднократно поднимался вопрос введения шаблонов в стандарт языка. Наткнувшись на стену отказа со стороны разработчиков языка MQL5,  я заинтересовался вопросом реализации шаблонов пользовательскими методами. Результаты моих поисков представлены в данной статье.


Немного истории С и C++

Язык С с самого начала создавался так, чтобы на нем можно было писать системные задачи. Создатели С не стали разрабатывать абстрактную модель исполнителя языка, а просто реализовали в нем те возможности, в которых более всего нуждались системные программисты. В первую очередь, это были средства непосредственной работы с памятью, структурные конструкции управления и модульная организация программы.

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

Итак, язык С возник как универсальный язык системного программирования. Но он не остался в этих рамках. К концу 80-х годов язык С, оттеснив Fortran с позиции лидера, завоевал массовую популярность среди программистов во всем мире и стал использоваться в самых различных прикладных задачах. Немалую роль здесь сыграло распространение Unix (а значит и С) в университетской среде, где проходило подготовку новое поколение программистов.

Но если все так безоблачно, то почему же еще продолжают использоваться все остальные языки, что поддерживает их существование? Ахиллесовой пятой языка С стало то, что он оказался слишком низкоуровневым для тех задач, которые поставили на повестку дня 90-е годы. У этой проблемы есть два аспекта.

С одной стороны, в язык были встроены слишком низкоуровневые средства: прежде всего, это работа с памятью и адресная арифметика. Недаром смена разрядности процессоров очень болезненно отражается на многих С-программах. С другой стороны, в языке С недостает  высокоуровневых средств - абстрактных типов данных и объектов, полиморфизма, обработки исключений. Как следствие, в программах на языке С техника реализации задачи часто доминирует над ее содержательной стороной.

Первые попытки исправить эти недостатки стали предприниматься еще в начале 80-х годов. Уже тогда Бьерн Страуструп в AT&T Bell Labs стал разрабатывать расширение языка С под условным названием "С с классами". Стиль ведения разработки вполне соответствовал духу, в котором создавался сам язык С - в него вводились те или иные возможности с целью сделать более удобной работу конкретных людей и групп.

Главное нововведение C++ - механизм классов, дающий возможность определять и использовать новые типы данных. Программист описывает внутреннее представление объекта класса и набор функций-методов для доступа к этому представлению. Одной из заветных целей при создании C++ было стремление увеличить процент повторного использования уже написанного кода.

Введение классов не исчерпывает всех новаций языка C++. В нем реализован полноценный механизм структурной обработки исключений (отсутствие которого в С значительно затрудняло написание надежных программ), механизм шаблонов и многое другое. Таким образом, генеральная линия развития языка была направлена на расширение его возможностей путем введения новых высокоуровневых конструкций при сохранении возможно полной совместимости с ANSI С


Шаблон как механизм макроподстановки

Чтобы разобраться в том, как можно реализовать шаблоны в MQL5, для начала нужно понять, как они работают в C++.

Обратимся к определению.

Шаблоны (англ. template) — средство языка C++, предназначенное для кодирования обобщенных алгоритмов, без привязки к некоторым параметрам (например, типам данных, размерам буферов, значениям по умолчанию).

В MQL5 шаблонов нет, но это не означает, что нет возможности пользоваться стилем программирования с использованием шаблонов. Механизм шаблонов в языке C++ - это, по сути, изощренный механизм макрогенерации, глубоко встроенный в язык. То есть, когда программист использует шаблон, компилятор определяет тип данных не в месте объявления, а в месте вызова функционала.

Шаблоны были введены в C++ для уменьшения количества кода, написанного программистом. Но не следует забывать о том, что код, набранный с клавиатуры программистом, не есть код, который будет создан компилятором. Сам механизм шаблонов не привел к уменьшению размера программ, а лишь уменьшил размер исходных кодов. Поэтому основной задачей, решаемой шаблонами, все же остается уменьшение количества кода, набранного программистом с клавиатуры.

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

Вторым аспектом введения механизма шаблонов в C++ является распределение памяти. Все дело в том, что в языке C память распределяется статично. Для того чтобы это распределение сделать более гибким, используется шаблон, задающий размер памяти для массива. Но этот аспект уже реализован разработчиками MQL4 в виде динамических массивов, равно как и динамических объектов в последующем языке MQL5.

Таким образом, нерешенной осталась задача подстановки типов. Разработчики MQL5 отказались решать этот вопрос, сославшись на то, что используя механизм подстановки шаблона, можно будет вскрыть код самого компилятора, и как следствие, написать декомпилятор.

Ну что же, им виднее. Нам остается лишь реализовать данную парадигму пользовательским образом.

Сразу оговорюсь, что мы не будем менять компилятор, и, тем более, править стандарт языка. Я предлагаю изменить подход к самим шаблонам. Если мы не можем создавать шаблоны на этапе компиляции, то машинное написание кода нам никто запретить не может. Я предлагаю перенести пользование шаблоном из участка генерации двоичного кода на участок написания текстового кода. Назовем такой подход "псевдошаблоны".


Псевдошаблоны

Псевдошаблон, в сравнении с шаблоном C++, имеет свои преимущества и свои недостатки. К недостаткам можно отнести дополнительные манипуляции с перемещением файлов. К преимуществам относятся более гибкие возможности, чем те, которые можно определить в стандарте языка. Перейдем от слов к делу.

Для того чтобы пользоваться псевдошаблоном, нам нужен аналог препроцессора. Им выступит скрипт Templates. Обрисую общие требования к скрипту: он должен прочитать указанный файл (сохраняя структуру данных), найти шаблон и заменить его на заданные типы.

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


Разработка кода скрипта

Определим требуемые входные переменные:

  1. Имя обрабатываемого файла.
  2. Переменная, хранящая перегружаемые типы данных.
  3. Имя шаблона, которое будет использоваться вместо реальных типов данных.
input string folder="Example templat";//имя обрабатываемого файла

input string type="long;double;datetime;string"
                 ;//имена пользовательских типов, delimit ";"
string TEMPLAT="_XXX_";// имя шаблона

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

При эксплуатации скрипта, я столкнулся с проблемой сбоев считывания маркеров.

Анализ показал, что при форматировании документа в MetaEditor к строке комментариев часто дописывался пробел или табуляция (зависит от ситуации). Проблема была решена удалением пробелов до и после значимых символов при определении маркера. В скрипте это реализовано автоматически, но теперь есть оговорка.

Имя маркера не может начинаться или оканчиваться пробелом.

Закрывающий маркер не обязателен, при его отсутствии код будет считываться до конца файла. Но без открывающего маркера не обойтись. Поскольку имена маркеров являются константами, то в моем коде вместо переменных я использую директиву предпроцессора #define

#define startread "//это_стартовая_точка"
#define endread "//это_конечная_точка"

Для формирования массива типов создана функция void ParserInputType(int i,string &type_d[],string text), в которой и происходит заполнение значений переменной type в массив type_dates[].    

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

Конечно, можно все сбросить в одну переменную, но тогда будут утеряны все переносы, и текст превратиться в бесконечную строку, поэтому в функции считывания файла используется именно массив строк, который изменяет свой размер на каждой итерации получения новой строки.

//+------------------------------------------------------------------+
//| считывание файла                                                 |
//+------------------------------------------------------------------+
void ReadFile()
  {
   string subfolder="Templates";
   int han=FileOpen(subfolder+"\\"+folder+".mqh",FILE_READ|FILE_SHARE_READ|FILE_TXT|FILE_ANSI,"\r"); 
   if(han!=INVALID_HANDLE)
     {
      string temp="";
      //--- перемотка файла до начальной точки
      do {temp=FileReadString(han);StringTrimLeft(temp);StringTrimRight(temp);}
      while(startread!=temp);

      string text=""; int size;
      //--- считывание файла в массив, до точки останова или до конца файла
      while(!FileIsEnding(han))
        {
         temp=text=FileReadString(han);
         // удаление символов табуляции для проверки выхода
         StringTrimLeft(temp);StringTrimRight(temp);
         if(endread==temp)break;
         // сброс данных в массив
         if(text!="")
           {
            size=ArraySize(fdates);
            ArrayResize(fdates,size+1);
            fdates[size]=text;
           }
        }
      FileClose(han);
     }
   else
     {
      Print("File open failed"+subfolder+"\\"+folder+".mqh, error",GetLastError());
      flagnew=true;
     }
  }

Для удобства пользования открытие файла происходит в режиме FILE_SHARE_READ. Это дает возможность запускать скрипт, не закрывая редактируемого файла. Расширение файла указано как mqh. Таким образом, скрипт напрямую читает текст кода, хранящийся во включаемом файле. Все дело в том, что файл с расширением mqh на самом деле является текстовым файлом, и в этом может убедиться любой, просто переименовав файл в txt, или открыв mqh файл с помощью любого текстового редактора. 

По окончании считывания массив имеет длину, равную количеству строк между открывающим и закрывающим маркером.

Имя открываемого файла должно иметь дополнение "templat", иначе исходный файл будет перезаписан с потерей исходных данных.

Теперь обратимся к анализу данных. Функции анализа и замены данных вызываются из функции записи в файл void WriteFile(int count). Комментарии приведены в теле функции.

void WriteFile(int count)
  {
   ...
   if(han!=INVALID_HANDLE)
     {
      if(flagnew)// если прочитать файл не удалось
        {
         ...
        }
      else
        {// если файл существует
         ArrayResize(tempfdates,count);
         int count_type=ArraySize(type_dates);
         //--- цикл переписывает содержание файла по каждому типу шаблона type_dates
         for(int j=0;j<count_type;j++)
           {
            for(int i=0;i<count;i++) // копируем данные во временный массив
               tempfdates[i]=fdates[i];
            for(int i=0;i<count;i++) // подменяем шаблоны типами
               Replace(tempfdates,i,j);

            for(int i=0;i<count;i++)
               FileWrite(han,tempfdates[i]); // сброс массива в файл
           }
        }
     ...
  }

Поскольку данные заменяются по месту и после преобразования массив будет изменен, то работать будем с копией. Здесь задаем размер массива временного хранения данных tempfdates[] и заполняем его по образцу fdates[].

После этого идет подстановка шаблонов с помощью функции Replace(). Параметрами функции выступают: обрабатываемый массив (где делается замена шаблона), счетчик строк i (для перемещения по массиву), а также счетчик типов j (для навигации по массиву типов).

Поскольку мы имеем два вложенных цикла, то исходный код печатается столько раз, сколько типов мы задали.

//+------------------------------------------------------------------+
//| подмена шаблонов типами                                          |
//+------------------------------------------------------------------+
void Replace(string &temp_m[],int i,int j)
  {
   if(i>=ArraySize(temp_m))return;
   if(j<ArraySize(type_dates))
      StringReplac(temp_m[i],TEMPLAT,type_dates[j]);// подмена templat типами   
  }

В функции Replace() стоят проверки (чтобы не было вызова несуществующего индекса массива) и вызывается вложенная функция StringReplac(). Имя функции не случайно выбрано похожим на стандартную функцию StringReplace, к тому же, количество параметров у них также одинаковое.

Таким образом, добавив всего одну букву "e", можно изменить логику замены. Стандартная функция берет значение образца find и заменяет его указанной строкой replacement. Моя же функция делает не просто замену, а анализирует, есть ли перед find символы (то есть, является ли find частью слова) и если да, то заменяет find на replacement, но в верхнем регистре, иначе замена происходит как есть. Таким образом, можно не только задавать типы, но и использовать их в именах перегружаемых данных.


Новшества

Теперь о нововведениях, которые вводились в процессе эксплуатации. Я уже упоминал выше, что при пользовании скриптом возникали проблемы со считыванием маркеров.

Проблема была решена следующим участком кода, находящимся в функции void ReadFile():

      string temp="";
      //--- промотка файла до начальной точки
      do {temp=FileReadString(han);StringTrimLeft(temp);StringTrimRight(temp);}
      while(startread!=temp);

Сам цикл поиска существовал и в первых версиях скрипта, но обрезка символов табуляции с помощью функций StringTrimLeft() и StringTrimRight() появилась только в доработанной версии.

Далее, к удобствам можно отнести и то, что теперь у выходного файла обрезается дополнение " templat" и исходящий файл сразу готов к подключению. Реализовано это с помощью функции удаления  заданного примера из указанной строки.

Код функции удаления:

//+------------------------------------------------------------------+
//| Удаление шаблона find из строки text                             |
//+------------------------------------------------------------------+
string StringDel(string text,const string find)
  {
   string str=text;
   StringReplace(str,find,"");
   return(str);
  }

Код, производящий обрезку имени файла, находится в функции void WriteFile(int count):

   string newfolder;
   if(flagnew)newfolder=folder;// если первый запуск создаем пустой файл заготовку под шаблоны
   else newfolder=StringDel(folder," templat");// иначе по шаблону создаем выходной файл

Кроме того, введен режим формирования заготовки. Если искомого файла в директории Files/Templates не существует, то будет сформирован файл-заготовка.

Пример:

//#define _XXX_ long
 
//это_стартовая_точка
 _XXX_
//это_конечная_точка

Код, формирующий эти строки, находится в функции void WriteFile(int count):

      if(flagnew)// если прочитать файл не удалось
        {// заполняем шаблонный файл заготовкой
         FileWrite(han,"#define "+TEMPLAT+" "+type_dates[0]);
         FileWrite(han," ");
         FileWrite(han,startread);
         FileWrite(han," "+TEMPLAT);
         FileWrite(han,endread);
         Print("Создаем заготовку шаблона "+subfolder+"\\"+folder+".mqh");
        }

Срабатывание кода защищено глобальной переменной flagnew, которая принимает значение true, если была ошибка чтения файла.

В процессе эксплуатации я ввел дополнительный шаблон. Подключение второго шаблона аналогично первому. Функции, в которых требуется делать изменения, перемещены поближе к функции OnStart() для подключения дополнительного шаблона. Тропка по подключению новых шаблонов протоптана. Таким образом, появилась возможность подключать нужное количество шаблонов по желанию пользователя. Перейдем к проверке работы.


Проверка работы

Для начала запустим скрипт с указанием требуемых параметров. В открывшемся окне укажем имя файла как "Example templat".

Заполним поля пользовательских типов данных через разделитель ";".

Стартовое окно скрипта

После нажатия OK формируется директория Templates, в которой создается файл-заготовка "Example templat.mqh".

Об этом событии нам говорит сообщение в журнале.

Сообщение в журнал

Внесем изменения в заготовку и запустим скрипт еще раз. На этот раз искомый файл уже существует в директории Templates (впрочем, как и сама директория), поэтому сообщения об ошибке открытия файла не будет. По заданному нами шаблону будет осуществлена замена:

//это_стартовая_точка
 _XXX_ Value_XXX_;
//это_конечная_точка

Откроем вновь созданный файл "Example.mqh".

 long ValueLONG;
 double ValueDOUBLE;
 datetime ValueDATETIME;
 string ValueSTRING;

Как видно из нашей строки, с шаблоном получилось 4 строки, по количеству типов, которые мы передали в параметре. Теперь в файле шаблона запишем две строки:

//это_стартовая_точка
 _XXX_ Value_XXX_;
 _XXX_ Type_XXX_;
//это_конечная_точка

Результат хорошо показывает логику работы скрипта.

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

 long ValueLONG;
 long TypeLONG;
 double ValueDOUBLE;
 double TypeDOUBLE;
 datetime ValueDATETIME;
 datetime TypeDATETIME;
 string ValueSTRING;
 string TypeSTRING;

Теперь включим в текст образца второй шаблон.

//это_стартовая_точка
 _XXX_ Value_XXX_(_xxx_ ind){return((_XXX_)ind);};
 _XXX_ Type_XXX_(_xxx_ ind){return((_XXX_)ind);};

 //это_конечная_точка

Результат:

 long ValueLONG(int ind){return((long)ind);};
 long TypeLONG(int ind){return((long)ind);};
 
 double ValueDOUBLE(float ind){return((double)ind);};
 double TypeDOUBLE(float ind){return((double)ind);};
 
 datetime ValueDATETIME(int ind){return((datetime)ind);};
 datetime TypeDATETIME(int ind){return((datetime)ind);};
 
 string ValueSTRING(string ind){return((string)ind);};
 string TypeSTRING(string ind){return((string)ind);};

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

Теперь хочу прояснить вопрос отладки кода. Приведенные примеры достаточно просты для отладки. В процессе программирования может понадобиться отладить достаточно большой кусок кода, а лишь потом его множить. Для этого в заготовке зарезервирована закомментированная строка "//#define _XXX_ long".

Уберем комментарии, и наш шаблон стал реальным типом. То есть мы просто сообщили компилятору, как нужно рассматривать данный шаблон.

К сожалению, отладить таким образом все типы вместе не получится. Но можно сделать отладку по одному типу, затем сменить в define тип шаблона и таким образом по очереди отладить все типы. Для отладки, конечно, придется переместить файл в директорию вызываемого файла или директорию Include. Именно об этом неудобстве отладки я и говорил выше, когда упоминал о недостатках псевдошаблонов.


Заключение

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

В моем скрипте реализована замена шаблонов поблочно. Но такой подход не является обязательным. Можно разработать более сложный анализатор, интерпретирующий определенные правила. В целом начало положено. Надеюсь на горячую дискуссию. В споре рождается истина. Удачи.