Простая форма #define

Простая форма директивы #define регистрирует идентификатор и последовательность символов, на которую этот идентификатор следует заменить везде в исходных кодах после директивы, вплоть до конца программы или до директивы #undef с тем же идентификатором.

Её синтаксис таков:

#define макро_идентификатор [текст]

Текст начинается после идентификатора и продолжается до конца текущей строки. Идентификатор и текст должно разделять произвольное количество пробелов или табуляций. Если требуемая последовательность символов слишком длинная, то для удобочитаемости можно переносить её на следующие строки, ставя в конце прерываемой строки символ обратной косой черты '\'.

#define макро_идентификатор текст_начало \
                            текст_продолжение \
                            текст_окончание

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

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

  • объявления флагов, которые затем используются для проверок условной компиляции;
  • объявления именованных констант;
  • сокращенной записи расхожих инструкций.

Первый пункт характерен тем, что после идентификатора можно ничего не указывать — наличия директивы с именем уже достаточно, чтобы соответствующий идентификатор был зарегистрирован и мог использоваться в условных директивах #ifdef/ifndef. Для них важно лишь, существует идентификатор или нет, то есть он работает в режиме флага: объявлен/не объявлен. Например, нижеприведенная директива определяет флаг DEMO:

#define DEMO

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

Второй вариант использования простой директивы позволяет заменить в исходном коде "магические числа" понятными именами. "Магическими числами" называют вставленные в исходный текст константы, смысл которых не всегда ясен (потому что число — это просто число: его желательно хотя бы пояснить в комментарии). Кроме того, одно и то же значение может быть разбросано по разным частям кода, и если программист решит изменить его на другое, то ему придется делать это во всех местах (и надеяться, что он ничего не упустил).

При наличии макроса с именем эти две проблемы легко решаются. Например, скрипт может подготавливать массив с числами ряда Фибоначчи на некую максимальную глубину. Тогда имеет смысл определить макрос с предопределенным размером массива и использовать его в описании самого массива (Preprocessor.mq5).

#define MAX_FIBO 10
 
int fibo[MAX_FIBO]; // 10
 
void FillFibo()
{
   int prev = 0;
   int result = 1;
 
   for(int i = 0i < MAX_FIBO; ++i// i < 10
   {
      int temp = result;
      result = result + prev;
      fibo[i] = result;
      prev = temp;
   }
}

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

Может возникнуть вопрос, чем определение через #define отличается от переменной-константы в глобальном контексте. Действительно, мы могли бы описать переменную с таким же именем и назначением, и даже сохранить верхний регистр букв:

const int MAX_FIBO = 10;

Однако в этом случае MQL5 не позволит определить массив с указанным размером, так как в квадратных скобках допускаются только константы, т.е. литералы (а переменная-константа, несмотря на свое похожее название, константой не является). Чтобы решить эту проблему, мы могли бы определить массив как динамический (без предварительного указания размера) и затем выделять для него память с помощью функции ArrayResize — здесь сложностей с передачей переменной в качестве размера не возникает.

Альтернативный способ определить именованную константу предоставляют перечисления, но он ограничен только целочисленными значениями. Например:

enum
{
   MAX_FIBO = 10
};

Макрос же способен содержать значение любого типа.

#define TIME_LIMIT     D'2023.01.01
#define MIN_GRID_STEP  0.005

Поиск имен макросов в исходных текстах для замены производится с учетом синтаксиса языка, то есть неделимые элементы, такие как идентификаторы переменных или строковые литералы останутся без изменений, даже если в их состав входит подстрока, совпадающая с одним из макросов. Например, с учетом нижеприведенного макроса XYZ, переменная XYZAXES сохранится как есть, а имя XYZ (поскольку оно целиком совпадает с макросом) будет заменено на ABC.

#define XYZ ABC
int XYZAXES = 3// int XYZAXES = 3
int XYZ = 0;     // int ABC = 0

С помощью макроподстановок можно встраивать свой код в исходный текст других программ. Обычно этот прием используется библиотеками, которые распространяются в виде заголовочных mqh-файлов и подключаются к программам с помощью директив #include.

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

void OnStart()
{
   Print("OnStart wrapper started");
   // ... дополнительные действия
   _OnStart();
   // ... дополнительные действия
   Print("OnStart wrapper stopped");
}
 
#define OnStart _OnStart

Допустим, эта часть находится в подключаемом заголовочном файле (Preprocessor.mqh).

Тогда оригинальная функция OnStartPreprocessor.mq5) будет переименована препроцессором в исходном коде на _OnStart (подразумевается, что там этот идентификатор не используется еще где-то по другому назначению). А новая версия OnStart из заголовка вызывает _OnStart, "обернув" её в дополнительные инструкции.

Третий распространенный способ использования простого #define — сокращение записи языковых конструкций. Например, заголовок бесконечного цикла можно обозначить одним словом LOOP:

#define LOOP for( ; !IsStopped() ; )

И потом применять в коде:

LOOP
{
   // ...
   Sleep(1000);
}

Данный способ также является основным приемом использования директивы #define с параметрами (см. далее).