Форма #define в виде псевдо-функции

Синтаксис параметрической формы #define напоминает функцию.

#define макро_идентификатор(параметр,...) текст_с_параметрами

Такой макрос имеет один или несколько параметров в круглых скобках. Параметры разделены запятыми. Каждый параметр — это простой идентификатор (часто однобуквенный). Причем у всех параметров одного макроса идентификаторы должны различаться.

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

После регистрации данной директивы препроцессор будет искать в исходных кодах строки вида:

макро_идентификатор(выражение,...)

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

Например, следующий макрос позволяет найти максимальное из двух значений:

#define MAX(A,B) ((A) > (B) ? (A) : (B))

Если в коде будет инструкция:

int z = MAX(xy);

она "раскроется" препроцессором в:

int z = ((x) > (y) ? (x) : (y));

Макроподстановка будет работать для любых типов данных (для которых допустимы примененные внутри макроса операции).

Вместе с тем, подстановка может иметь и побочные эффекты. Например, если в качестве фактического параметра указан вызов функции или инструкция с модификацией переменной (скажем, ++x), то соответствующее действие может быть выполнено несколько раз (вместо предполагаемого одного раза). В случае MAX это случится дважды: во время сравнения и при получении значений в одной из веток оператора '?:'. В связи с этим имеет смысл по возможности преобразовывать подобные макросы в функции (особенно учитывая, что в MQL5 для функций автоматически применяется инлайнинг).

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

#define MUL(A,BA * B

Тогда использование макроса со следующими выражениями приведет к неожиданному результату:

int x = MUL(1 + 23 + 4); // 1 + 2 * 3 + 4

Вместо умножения (1 + 2) * (3 + 4), что дает 21, мы получили 1 + 2 * 3 + 4, то есть 11. Свободное от этой проблемы определение макроса должно быть таким:

#define MUL(A,B) ((A) * (B))

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

#define SQ3(X) (X * X * X)
#define ABS(XMathAbs(SQ3(X))
#define INC(Y) (++(Y))

Тогда следующий код выведет 504 (MathAbs — встроенная функция возвращающая модуль числа, т.е. без знака):

int x = -10;
Print(ABS(INC(x)));
// -> ABS(++(Y))
// -> MathAbs(SQ3(++(Y)))
// -> MathAbs((++(Y))*(++(Y))*(++(Y)))
// -> MathAbs(-9*-8*-7)
// -> 504

В переменой x при этом останется значение -7 (за счет троекратного инкремента).

Определение макроса может содержать непарные скобки. Такой прием используется, как правило, в паре макросов, один из которых должен открывать некий фрагмент кода, а другой закрывать. При этом непарные в каждом из них скобки станут парными. В частности, в файлах стандартной библиотеки из поставки MetaTrader 5, в Controls/Defines.mqh определены макросы EVENT_MAP_BEGIN и EVENT_MAP_END. С помощью них формируется функция обработки событий в графических объектах.

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

Рассмотрим пример.

#define NEG(x) (-SQN(x))*TEN
#define SQN(x) ((x)*(x))
#define TEN 10
...
Print(NEG(2)); // -40

Здесь макрос NEG использует макросы SQN и TEN, которые описаны ниже него. И это не мешает успешно применять его в коде после всех трех #define-ов.

Однако если изменить относительное положение строк на следующее:

#define NEG(x) (-SQN(x))*TEN
#define SQN(x) ((x)*(x))
...
Print(NEG(2)); // error: 'TEN' - undeclared identifier
...
#define TEN 10

получим ошибку компиляции "неизвестный идентификатор" ("undeclared identifier").