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

  • Он не похож на весь остальной язык

  • Макросы могут возвращать неполные синтаксические конструкции, или вовсе различные, в зависимости от параметров

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

Со всеми его недостатками, инструмент есть в языке и достоин изучения.

Рекурсия
Небольшие полезные макросы:

Построение макроса высшего порядка MAP
Написание парсера на препроцессоре
Заключение

❯ Рекурсия

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

Попробуем её реализовать.

#define A(x) x B(x)
#define B(x) x A(x)

A(0)

Ожидается бесконечная рекурсия, но она не происходит. По итогу, получится нечто вроде:

0 0 A(0)

Препроцессор не выполнил следующий шаг, а просто остановился. Происходит это в связи со следующим в стандарте C23 (стандарт C++ просто копирует стандарт C для описания препроцессора):

6.10.4.4.1

After all parameters in the replacement list have been substituted and # and ## processing has
taken place, all placemarker preprocessing tokens are removed. The resulting preprocessing token
sequence is then rescanned, along with all subsequent preprocessing tokens of the source file, for
more macro names to replace.

6.10.4.4.2

If the name of the macro being replaced is found during this scan of the replacement list (not
including the rest of the source file’s preprocessing tokens), it is not replaced. Furthermore, if any
nested replacements encounter the name of the macro being replaced, it is not replaced. These
nonreplaced macro name preprocessing tokens are no longer available for further replacement even
if they are later (re)examined in contexts in which that macro name preprocessing token would
otherwise have been replaced.

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

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

#define OUT
#define A(x) x B OUT (x)
#define B(x) x A OUT (x)

После выполнение данных действий, если просто вызвать макрос A, то окажется выполненным лишь один шаг. Для продолжения выполнения, нужно вызвать сканирования результата после выполнения макроса, для этого необходимо задать макрос EVAL и применить его.

#define EVAL(...) __VA_ARGS__

Если всё объединить вместе, то получится следующее:

#define OUT
#define A(x) x B OUT (x)
#define B(x) x A OUT (x)

#define EVAL(...) __VA_ARGS__

A(0)
EVAL(A(0))
EVAL(EVAL(A(0)))
EVAL(EVAL(EVAL(A(0))))

В результате получается:

0 B (0)
0 0 A (0)
0 0 0 B (0)
0 0 0 0 A (0)

Во избежание захламления кода множеством EVAL, можно сделать несколько макросов EVAL_0, каждый будет вызывать несколько макросов EVAL_1 внутри и так далее:

#define EVAL_2(...) __VA_ARGS__
#define EVAL_1(...) EVAL_2(EVAL_2(EVAL_2(EVAL_2(__VA_ARGS__))))
#define EVAL_0(...) EVAL_1(EVAL_1(EVAL_1(EVAL_1(__VA_ARGS__))))
#define EVAL(...) EVAL_0(EVAL_0(EVAL_0(EVAL_0(__VA_ARGS__))))

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

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

❯ Небольшие полезные макросы

Макрос, что возвращает пустоту

#define VOID(...)

Макрос, возвращающий первый список из всех аргументов

Необходимо определить макрос, который будет извлекать первый список из аргументов, например, "(123) other", возвращая "(123)". Для достижения этой цели можно добавить дополнительный макрос в начале, чтобы после его раскрытия вызывался второй макрос с аргументами 123. Этот второй макрос будет добавлять VOID после необходимого результата, что позволит устранить все лишние элементы.

#define GET_FIRST_LIST_0(...) (__VA_ARGS__) VOID (
#define GET_FIRST_LIST(...) GET_FIRST_LIST_0 __VA_ARGS__ )
/*

GET_FIRST_LIST((1, 2, 3), some, other)
 => GET_FIRST_LIST_0 (1, 2, 3), some, other )
 => (1, 2, 3) VOID( some, other )
 => (1, 2, 3)
*/

Макросы с задержкой

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

#define DELAY_COMMA_0() ,
#define DELAY_COMMA_1() DELAY_COMMA_0 OUT()
#define DELAY_COMMA_2() DELAY_COMMA_1 OUT()
#define DELAY_COMMA_3() DELAY_COMMA_2 OUT()

#define DELAY_OPEN_BRACE_0() (
#define DELAY_OPEN_BRACE_1() DELAY_OPEN_BRACE_0 OUT()
#define DELAY_OPEN_BRACE_2() DELAY_OPEN_BRACE_1 OUT()
#define DELAY_OPEN_BRACE_3() DELAY_OPEN_BRACE_2 OUT()

#define DELAY_CLOSE_BRACE_0() )
#define DELAY_CLOSE_BRACE_1() DELAY_CLOSE_BRACE_0 OUT()
#define DELAY_CLOSE_BRACE_2() DELAY_CLOSE_BRACE_1 OUT()
#define DELAY_CLOSE_BRACE_3() DELAY_CLOSE_BRACE_2 OUT()

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

Альтернативный вычисляющий макрос

#define EVAL2(...) __VA_ARGS__

Функциональность данного макроса аналогична макросу EVAL_2, однако использование макросов EVAL не всегда возможно. Это связано с тем, что данный макрос может выполняться внутри более сложной конструкции, которая, в свою очередь, будет вычисляться внутри EVAL. Согласно пункту 6.10.4.4.2 стандарта C23, препроцессор отклонит этот вызов и не выполнит его. Поэтому в тех случаях, когда использование стандартных макросов EVAL невозможно, будет применяться данный альтернативный макрос.

Макрос для конкатенации значений

В препроцессоре уже существует оператор ## для конкатенации. Однако при его использовании совместно с макросами, он просто соединяет их, не производя предварительного вычисления значений. Для решения этой проблемы задается макрос CONCAT, который будет пересканировать значения перед конкатенацией:

#define CONCAT_1(a, b) a##b
#define CONCAT_0(...) CONCAT_1 OUT(__VA_ARGS__)
#define CONCAT(...) EVAL2(CONCAT_0(__VA_ARGS__))

В данном случае, макрос CONCAT_0 вернет невыполненный макрос CONCAT_1, тогда как макрос CONCAT вернет соединенные значения.

If-else макрос

Часто возникает необходимость в макросе, который будет проверять, равно ли переданное значение заранее определенному значению или значениям. Для этой цели определяется макрос IF_ELSE.

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

Финальный макрос IF_ELSE_1 просто возвращает второй аргумент. Если количество аргументов изменится, то второй аргумент будет состоять из части макроса и второго значения из IF_ELSE. Если же количество аргументов остаётся неизменным, то это будет третий аргумент IF_ELSE:

#define IF_ELSE_1(test, next, ...) next OUT
#define IF_ELSE_0(test, next, ...) IF_ELSE_1(test, next, __VA_ARGS__, 0)

#define IF_ELSE(condition, t, f) IF_ELSE_0(condition OUT t, f)

#define CHECK_IF_ELSE 0,

/*
IF_ELSE(CHECK_IF_ELSE, true, false)
 => IF_ELSE_0(0, OUT true, false)
 => IF_ELSE_1(0, true, false, 0)
 => true OUT

IF_ELSE(FALSE, true, false)
 => IF_ELSE_0(FALSE OUT true, false)
 => IF_ELSE_1(FALSE true, false, 0)
 => false OUT
*/

❯ Построение макроса высшего порядка MAP

Цель данного раздела заключается в разработке макроса MAP, который применяет переданный макрос ко всем элементам списка. Это аналогично поведению функции map() во многих языках программирования (в C++ это std::views::transform).

Идея реализации состоит в определении нескольких макросов: MAP_0 и MAP_1. Эти макросы будут поочередно передавать управление друг другу и применять заданный макрос к текущему элементу.

Однако, если попытаться реализовать это сразу напрямую, возникнет проблема, заключающаяся в том, что в конце останется либо MAP_1, либо MAP_0.

#define MAP_0(macro, x, ...) macro(x), MAP_1 OUT(macro, __VA_ARGS__)
#define MAP_1(macro, x, ...) macro(x), MAP_0 OUT(macro, __VA_ARGS__)


EVAL(MAP_1(MACRO, 1, 2, 3)) // MACRO(1), MACRO(2), MACRO(3), MAP_2 (MACRO, )

Чтобы избежать подобной ситуации, необходимо определить конец списка и перед ним использовать не MAP_0 или MAP_1, а макрос VOID. Для различения конца списка от его продолжения в конце добавляется дополнительный элемент "()()". Таким образом, задача определения следующего макроса сводится к проверке, является ли следующий элемент "()()".

Для этой цели мы используется макрос IF_ELSE. В true-ветвлении условия передаем VOID, а в false-ветвление -- добавляем запятую и следующий макрос next, где next -- это следующий макрос (MAP_0 или MAP_1).

Для проверки зададим макрос MAP_CHECK_0, который будет возвращать макрос MAP_CHECK_1. Если последний сработает, он увеличит количество элементов:

#define MAP_CHECK_1() 0,
#define MAP_CHECK_0() MAP_CHECK_1

/*
MAP_CHECK_0()()
 => MAP_CHECK_1()
 => 0,
*/


#define MAP_NEXT(test, next) IF_ELSE(MAP_CHECK_0 test, VOID, DELAY_COMMA_2() next)
/*

MAP_NEXT(()(), next)
 => IF_ELSE(MAP_CHECK_0 ()(), VOID, DELAY_COMMA_2() next)
 => VOID

MAP_NEXT(some, next)
 => IF_ELSE(MAP_CHECK_0 some, VOID, DELAY_COMMA_2() next)
 => , next
*/

Теперь остается лишь определить макросы MAP_0, MAP_1 и финальный макрос MAP, который будет использовать EVAL для вычисления:

#define MAP_0(macro, x, next, ...) macro(x) MAP_NEXT(next, MAP_1)(macro, next, __VA_ARGS__)
#define MAP_1(macro, x, next, ...) macro(x) MAP_NEXT(next, MAP_0)(macro, next, __VA_ARGS__)


#define MAP(macro, ...) EVAL(MAP_0(macro, __VA_ARGS__, ()(), 0))


#define MACRO(x) x
/*
MAP(MACRO, 1, 2)
 => MAP_0(MACRO, 1, 2, ()(), 0)
 => MACRO(1) MAP_NEXT(2, MAP_1) (MACRO, 2, ()(), 0)
 => 1 , MAP_1(MACRO, 2, ()(), 0)
 => 1 , MACRO(2) MAP_NEXT(()(), MAP_0) (MACRO, ()(), 0)
 => 1 , 2 VOID(MACRO, ()(), 0)
 => 1 , 2
*/

❯ Построение простого парсера на препроцессоре

Задача заключается в преобразовании следующего кода:

PARSE((1, 2, 3)A(4, 5, 6) B A(7, 8, 9) other code,
     () other code);

в результат, аналогичный следующему:

 BLOCK(ELEMENT_A((1, 2, 3), 4, 5, 6), ELEMENT_B, ELEMENT_A((1, 2, 3), 7, 8, 9), BASIC_PART ((1, 2, 3), other code))
,BLOCK(BASIC_PART ((), other code))

Хотя задача не является сложной, она уже не является тривиальной, как в случае с предыдущими задачами. Разобьем эту задачу на несколько частей. Поскольку мы работаем не с обычными списками аргументов препроцессора, для реализации потребуются вспомогательные макросы CLEAR и REMOVE_FIRST:

  • макрос CLEAR будет удалять все элементы, кроме первого, который может быть распарсен. Если таких не окажется, он вернет ERROR;

  • макрос REMOVE_FIRST будет убирать первый элемент, что возможно распарсить.

CLEAR(A(1, 2, 3) B code)
 => A(1, 2, 3)
CLEAR(B code)
 => B
CLEAR(code)
 => ERROR


REMOVE_FIRST(A(1, 2, 3) B code)
 => B code
REMOVE_FIRST(B code)
 => code

Также потребуется макрос, который будет определять, является ли текущий элемент концом списка. В таком случае, он будет возвращать BASIC_PART, а в противном случае — следующий макрос:

PARSE_NEXT(PARSE_BLOCK_1, A(4, 5, 6) B code)
 => PARSE_BLOCK_1
PARSE_NEXT(PARSE_BLOCK_1, B code)
 => PARSE_BLOCK_1
PARSE_NEXT(PARSE_BLOCK_1, code)
 => BASIC_PART

Кроме того, понадобится макрос, который будет парсить элемент блока (не финальный) — PARSE_BLOCK_ELEMENT:

PARSE_BLOCK_ELEMENT((1, 2, 3), A(4, 5, 6))
 => ELEMENT_A((1, 2, 3), 4, 5, 6)

PARSE_BLOCK_ELEMENT((1, 2, 3), B)
 => ELEMENT_B

Далее макросы, которые будут поочередно передавать управление друг другу — PARSE_BLOCK_1 и PARSE_BLOCK_2. Макрос, который будет парсить блок целиком, назовем PARSE_BLOCK. Наконец, макрос PARSE будет применять PARSE_BLOCK ко всем блокам.

REMOVE_FIRST

Идея реализации заключается в том, чтобы соединить REMOVE_ с переданным аргументом. В результате могут получиться REMOVE_A(...) и REMOVE_B. После их выполнения не должно остаться ничего от них, поэтому после соединения, препроцессор просканирует результат REMOVE_FIRST и заменит их на пустоту.

#define REMOVE_A(...)
#define REMOVE_B

#define REMOVE_FIRST(x) REMOVE_##x

CLEAR

Его реализовать можно при помощи IF_ELSE и макросов проверок. Его имплементация чем-то похожа на имплементацию GET_FIRST_LIST, чем-то - на REMOVE_FIRST

Макросы проверок будут увеличивать кол-во элементов для IF_ELSE, оставлять A(...) или B и добавлять VOID(, чтобы убрать всё лишнее. Так их и зададим, но чтобы закрывающая скобка не сломала логику внутри IF_ELSE, там будет задержка 2

#define PARSE_CHECK_0_A(...) 0, A(__VA_ARGS__) VOID DELAY_OPEN_BRACE_2()

#define PARSE_CHECK_0_B 0, B VOID DELAY_OPEN_BRACE_2()

В IF_ELSE для true-ветвления будет использована закрывающая скобка с задержкой 2, а для ложного — ERROR:


#define CLEAR(x) IF_ELSE(PARSE_CHECK_0_##x, DELAY_CLOSE_BRACE_2(), ERROR)

/*
CLEAR(A(1, 2, 3) B code)
 => IF_ELSE(PARSE_CHECK_0_A(1, 2, 3) B code, DELAY_CLOSE_BRACE_2(), ERROR)
 => A(1, 2, 3) VOID (B code)
 => A(1, 2, 3)

CLEAR(B code)
 => IF_ELSE(PARSE_CHECK_0_B code, DELAY_CLOSE_BRACE_2(), ERROR)
 => B VOID (code)
 => B

CLEAR(code)
 => IF_ELSE(PARSE_CHECK_0_code, DELAY_CLOSE_BRACE_2(), ERROR)
 => ERROR
*/

PARSE_NEXT

Для реализации данного макроса используем макрос IF_ELSE и макросы для проверок. Эти макросы будут активироваться для аргументов A(...) и B, увеличивая количество аргументов. Для соединения значений применяется макрос CONCAT. В конструкции IF_ELSE проверяется количество аргументов, и в зависимости от результата возвращается либо указанный макрос, либо BASIC_PART:

#define PARSE_CHECK_1_A(...) 0,
#define PARSE_CHECK_1_B 0,


#define PARSE_NEXT(macro, ...) IF_ELSE(CONCAT(PARSE_CHECK_1_, CLEAR(__VA_ARGS__)), macro, BASIC_PART)
/*

PARSE_NEXT(PARSE_BLOCK_1, A(1, 2, 3) B code)
 => IF_ELSE(CONCAT(PARSE_CHECK_1_, CLEAR(A(1, 2, 3) B code)), PARSE_BLOCK_1, BASIC_PART)
 => IF_ELSE(PARSE_CHECK_1_A(1, 2, 3), PARSE_BLOCK_1, BASIC_PART)
 => PARSE_BLOCK_1


PARSE_NEXT(PARSE_BLOCK_1, B code)
 => IF_ELSE(CONCAT(PARSE_CHECK_1_, CLEAR(B code)), PARSE_BLOCK_1, BASIC_PART)
 => IF_ELSE(PARSE_CHECK_1_B, PARSE_BLOCK_1, BASIC_PART)
 => PARSE_BLOCK_1


PARSE_NEXT(PARSE_BLOCK_1, code)
 => IF_ELSE(CONCAT(PARSE_CHECK_1_, CLEAR(code)), PARSE_BLOCK_1, BASIC_PART)
 => IF_ELSE(PARSE_CHECK_1_ERROR, PARSE_BLOCK_1, BASIC_PART)
 => BASIC_PART
*/

PARSE_BLOCK_ELEMENT

Для реализации данного макроса необходимо определить макросы проверок, которые будут добавлять открывающую скобку и передавать необходимые аргументы для выполнения макросов трансформации:

#define PARSE_CHECK_2_A(...) TRANSFORM_A OUT ((__VA_ARGS__),

#define PARSE_CHECK_2_B TRANSFORM_B OUT (

Затем следует реализовать сам макрос PARSE_BLOCK_ELEMENT. Для этого необходимо сконкатенировать значения этого макроса с очищенными значениями, полученными с помощью CLEAR, добавив список и закрывающую скобку, чтобы все выполнялось корректно:

#define PARSE_BLOCK_ELEMENT(list, ...) CONCAT(PARSE_CHECK_2_, CLEAR(__VA_ARGS__)) list)

/*
PARSE_BLOCK_ELEMENT((1, 2, 3), A(4, 5, 6) B code)
 => CONCAT(PARSE_CHECK_2_, CLEAR(A(4, 5, 6) B code)) (1, 2, 3))
 => PARSE_CHECK_2_A(4, 5, 6) (1, 2, 3))
 => TRANSFORM_A((4, 5, 6), (1, 2, 3))

PARSE_BLOCK_ELEMENT((1, 2, 3), B code)
 => CONCAT(PARSE_CHECK_2_, CLEAR(B code)) (1, 2, 3))
 => PARSE_CHECK_2_B (1, 2, 3))
 => TRANSFORM_B((1, 2, 3))
*/

Далее необходимо определить макросы TRANSFORM_A и TRANSFORM_B. В макросе TRANSFORM_A требуется удалить скобки, для чего будет использован макрос EVAL2:

#define TRANSFORM_A(first, second) ELEMENT_A(second, EVAL2 first)
/*
TRANSFORM_A((4, 5, 6), (1, 2, 3))
 => ELEMENT_A((1, 2, 3), EVAL2 (4, 5, 6))
 => ELEMENT_A((1, 2, 3), 4, 5, 6)
*/

#define TRANSFORM_B(...) ELEMENT_B
/*
TRANSFORM_B((1, 2, 3))
 => ELEMENT_B
*/

Финальная часть

Осталось лишь объединить все определенные макросы вместе.

Определим макрос PARSE_BLOCK, который оборачивает все в BLOCK(), отделяет первый список от остальных элементов и передает управление в PARSE_BLOCK_0. Этот макрос также определяет наличие в списке элементов, отличных от BASIC_PART. Если такие элементы присутствуют, управление передается в PARSE_BLOCK_1, в противном случае -- в BASIC_PART:

#define PARSE_BLOCK_0(list, ...) PARSE_NEXT(PARSE_BLOCK_1, __VA_ARGS__)(list, __VA_ARGS__)

#define PARSE_BLOCK(...) BLOCK(PARSE_BLOCK_0(GET_FIRST_LIST(__VA_ARGS__), VOID __VA_ARGS__))

Далее определим макросы PARSE_BLOCK_1 и PARSE_BLOCK_2. Эти макросы будут обрабатывать текущий элемент, затем выбирать следующий макрос через PARSE_NEXT и передавать управление ему:

#define PARSE_BLOCK_2(list, ...) \
 PARSE_BLOCK_ELEMENT(list, CLEAR(__VA_ARGS__)), PARSE_NEXT(PARSE_BLOCK_1, REMOVE_FIRST(__VA_ARGS__))(list, REMOVE_FIRST(__VA_ARGS__))

#define PARSE_BLOCK_1(list, ...) \
 PARSE_BLOCK_ELEMENT(list, CLEAR(__VA_ARGS__)), PARSE_NEXT(PARSE_BLOCK_2, REMOVE_FIRST(__VA_ARGS__))(list, REMOVE_FIRST(__VA_ARGS__))

Наконец, реализуем макрос PARSE. Для его реализации используем MAP. Поскольку MAP уже использует EVAL, нет необходимости добавлять дополнительный уровень EVAL:

#define PARSE(...) MAP(PARSE_BLOCK, __VA_ARGS__)
Полный код из статьи
#define OUT

#define EVAL_2(...) __VA_ARGS__
#define EVAL_1(...) EVAL_2(EVAL_2(EVAL_2(EVAL_2(__VA_ARGS__))))
#define EVAL_0(...) EVAL_1(EVAL_1(EVAL_1(EVAL_1(__VA_ARGS__))))
#define EVAL(...) EVAL_0(EVAL_0(EVAL_0(EVAL_0(__VA_ARGS__))))

#define VOID(...)

#define GET_FIRST_LIST_0(...) (__VA_ARGS__) VOID (

#define GET_FIRST_LIST(...) GET_FIRST_LIST_0 __VA_ARGS__ )
/*
GET_FIRST_LIST((1, 2, 3), some, other)
  => GET_FIRST_LIST_0 (1, 2, 3), some, other )
  => (1, 2, 3) VOID( some, other )
  => (1, 2, 3)
*/

#define DELAY_COMMA_0() ,
#define DELAY_COMMA_1() DELAY_COMMA_0 OUT()
#define DELAY_COMMA_2() DELAY_COMMA_1 OUT()
#define DELAY_COMMA_3() DELAY_COMMA_2 OUT()

#define DELAY_OPEN_BRACE_0() (
#define DELAY_OPEN_BRACE_1() DELAY_OPEN_BRACE_0 OUT()
#define DELAY_OPEN_BRACE_2() DELAY_OPEN_BRACE_1 OUT()
#define DELAY_OPEN_BRACE_3() DELAY_OPEN_BRACE_2 OUT()

#define DELAY_CLOSE_BRACE_0() )
#define DELAY_CLOSE_BRACE_1() DELAY_CLOSE_BRACE_0 OUT()
#define DELAY_CLOSE_BRACE_2() DELAY_CLOSE_BRACE_1 OUT()
#define DELAY_CLOSE_BRACE_3() DELAY_CLOSE_BRACE_2 OUT()

#define EVAL2(...) __VA_ARGS__

#define CONCAT_1(a, b) a##b

#define CONCAT_0(...) CONCAT_1 OUT(__VA_ARGS__)

#define CONCAT(...) EVAL2(CONCAT_0(__VA_ARGS__))

#define IF_ELSE_1(test, next, ...) next OUT
#define IF_ELSE_0(test, next, ...) IF_ELSE_1(test, next, __VA_ARGS__, 0)

#define IF_ELSE(condition, t, f) IF_ELSE_0(condition OUT t, f)

/*
#define CHECK_IF_ELSE 0,

IF_ELSE(CHECK_IF_ELSE, true, false)
  => IF_ELSE_0(0, OUT true, false)
  => IF_ELSE_1(0, true, false, 0)
  => true OUT

IF_ELSE(FALSE, true, false)
  => IF_ELSE_0(FALSE OUT true, false)
  => IF_ELSE_1(FALSE true, false, 0)
  => false OUT
*/

#define MAP_CHECK_1() 0,
#define MAP_CHECK_0() MAP_CHECK_1

/*
MAP_CHECK_0()()
  => MAP_CHECK_1()
  => 0,
*/

#define MAP_NEXT(test, next) IF_ELSE(MAP_CHECK_0 test, VOID, DELAY_COMMA_2() next)

/*
MAP_NEXT(()(), next)
  => IF_ELSE(MAP_CHECK_0 ()(), VOID, DELAY_COMMA_2() next)
  => VOID

MAP_NEXT(some, next)
  => IF_ELSE(MAP_CHECK_0 some, VOID, DELAY_COMMA_2() next)
  => , next
*/

#define MAP_0(macro, x, next, ...) macro(x) MAP_NEXT(next, MAP_1)(macro, next, __VA_ARGS__)
#define MAP_1(macro, x, next, ...) macro(x) MAP_NEXT(next, MAP_0)(macro, next, __VA_ARGS__)

#define MAP(macro, ...) EVAL(MAP_0(macro, __VA_ARGS__, ()(), 0))
/*
#define MACRO(x) x
MAP(MACRO, 1, 2)
  => MAP_0(MACRO, 1, 2, ()(), 0)
  => MACRO(1) MAP_NEXT(2, MAP_1) (MACRO, 2, ()(), 0)
  => 1 , MAP_1(MACRO, 2, ()(), 0)
  => 1 , MACRO(2) MAP_NEXT(()(), MAP_0) (MACRO, ()(), 0)
  => 1 , 2 VOID(MACRO, ()(), 0)
  => 1 , 2
*/

#define REMOVE_A(...)
#define REMOVE_B

#define REMOVE_FIRST(x) REMOVE_##x

#define PARSE_CHECK_0_A(...) 0, A(__VA_ARGS__) VOID DELAY_OPEN_BRACE_2()

#define PARSE_CHECK_0_B 0, B VOID DELAY_OPEN_BRACE_2()

#define CLEAR(x) IF_ELSE(PARSE_CHECK_0_##x, DELAY_CLOSE_BRACE_2(), ERROR)
/*
CLEAR(A(1, 2, 3) B code)
  => IF_ELSE(PARSE_CHECK_0_A(1, 2, 3) B code, DELAY_CLOSE_BRACE_2(), ERROR)
  => A(1, 2, 3) VOID (B code)
  => A(1, 2, 3)
CLEAR(B code)
  => IF_ELSE(PARSE_CHECK_0_B code, DELAY_CLOSE_BRACE_2(), ERROR)
  => B VOID (code)
  => B
CLEAR(code)
  => IF_ELSE(PARSE_CHECK_0_code, DELAY_CLOSE_BRACE_2(), ERROR)
  => ERROR
*/

#define PARSE_CHECK_1_A(...) 0,

#define PARSE_CHECK_1_B 0,

#define PARSE_NEXT(macro, ...) IF_ELSE(CONCAT(PARSE_CHECK_1_, CLEAR(__VA_ARGS__)), macro, BASIC_PART)

/*
PARSE_NEXT(PARSE_BLOCK_1, A(1, 2, 3) B code)
  => IF_ELSE(CONCAT(PARSE_CHECK_1_, CLEAR(A(1, 2, 3) B code)), PARSE_BLOCK_1, BASIC_PART)
  => IF_ELSE(PARSE_CHECK_1_A(1, 2, 3), PARSE_BLOCK_1, BASIC_PART)
  => PARSE_BLOCK_1

PARSE_NEXT(PARSE_BLOCK_1, B code)
  => IF_ELSE(CONCAT(PARSE_CHECK_1_, CLEAR(B code)), PARSE_BLOCK_1, BASIC_PART)
  => IF_ELSE(PARSE_CHECK_1_B, PARSE_BLOCK_1, BASIC_PART)
  => PARSE_BLOCK_1
PARSE_NEXT(PARSE_BLOCK_1, code)
  => IF_ELSE(CONCAT(PARSE_CHECK_1_, CLEAR(code)), PARSE_BLOCK_1, BASIC_PART)
  => IF_ELSE(PARSE_CHECK_1_ERROR, PARSE_BLOCK_1, BASIC_PART)
  => BASIC_PART
*/

#define PARSE_CHECK_2_A(...) TRANSFORM_A OUT ((__VA_ARGS__),

#define PARSE_CHECK_2_B TRANSFORM_B OUT (

#define PARSE_BLOCK_ELEMENT(list, ...) CONCAT(PARSE_CHECK_2_, CLEAR(__VA_ARGS__)) list)
/*
PARSE_BLOCK_ELEMENT((1, 2, 3), A(4, 5, 6) B code)
  => CONCAT(PARSE_CHECK_2_, CLEAR(A(4, 5, 6) B code)) (1, 2, 3))
  => PARSE_CHECK_2_A(4, 5, 6) (1, 2, 3))
  => TRANSFORM_A((4, 5, 6), (1, 2, 3))

PARSE_BLOCK_ELEMENT((1, 2, 3), B code)
  => CONCAT(PARSE_CHECK_2_, CLEAR(B code)) (1, 2, 3))
  => PARSE_CHECK_2_B (1, 2, 3))
  => TRANSFORM_B((1, 2, 3))
*/

#define TRANSFORM_A(first, second) ELEMENT_A(second, EVAL2 first)
/*
TRANSFORM_A((4, 5, 6), (1, 2, 3))
  => ELEMENT_A((1, 2, 3), EVAL2 (4, 5, 6))
  => ELEMENT_A((1, 2, 3), 4, 5, 6)
*/
#define TRANSFORM_B(...) ELEMENT_B
/*
TRANSFORM_B((1, 2, 3))
  => ELEMENT_B
*/

#define PARSE_BLOCK_2(list, ...) \
  PARSE_BLOCK_ELEMENT(list, CLEAR(__VA_ARGS__)), PARSE_NEXT(PARSE_BLOCK_1, REMOVE_FIRST(__VA_ARGS__))(list, REMOVE_FIRST(__VA_ARGS__))
#define PARSE_BLOCK_1(list, ...) \
  PARSE_BLOCK_ELEMENT(list, CLEAR(__VA_ARGS__)), PARSE_NEXT(PARSE_BLOCK_2, REMOVE_FIRST(__VA_ARGS__))(list, REMOVE_FIRST(__VA_ARGS__))

#define PARSE_BLOCK_0(list, ...) PARSE_NEXT(PARSE_BLOCK_1, __VA_ARGS__)(list, __VA_ARGS__)

#define PARSE_BLOCK(...) BLOCK(PARSE_BLOCK_0(GET_FIRST_LIST(__VA_ARGS__), VOID __VA_ARGS__))

#define PARSE(...) MAP(PARSE_BLOCK, __VA_ARGS__)

PARSE((1, 2, 3) A(4, 5, 6) B A(7, 8, 9) other code,
      () other code);

❯ Заключение

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

Код из статьи на годболте

Комментарии (15)


  1. sergio_nsk
    28.07.2025 14:33

    Что такое C/C++? Я знаю только препроцессор языка C. На stackoveflow гнобяд за несуществующий язык программирования C/C++.


    1. j4niwzis Автор
      28.07.2025 14:33

      Языка C/C++ и правда нету. Но в статье используется подмножество языка C++, которое можно использовать и в C. Если же указать просто C, то это не будет включать C++(ведь не весь код на C будет работать в C++), а включать нужно.


      1. sergio_nsk
        28.07.2025 14:33

        Где в статье подмножество какого-либо языка? Все примеры - только макросы, и они обрабатываются программой cpp, препроцессором языка C.


        1. j4niwzis Автор
          28.07.2025 14:33

          Где в статье подмножество какого-либо языка?

          Оно неявно получается из используемых возможностей языка в статье. Всё из статьи можно использовать как в C++, так в C. Тут ограничения языков C и C++ только тем, что можно использовать и там и там. Т.е. подмножество.

          обрабатываются программой cpp

          Что это за программа такая? Да и обрабатываться они могут вообще чем угодно, стандарт про это вообще никак не говорит.

          препроцессором языка C

          Что-то не вижу в стандарте C++ препроцессора языка Си. Там идёт описание работы препроцессора, следовательно, препроцессор это языка C++, если в контексте этот язык.


  1. dersoverflow
    28.07.2025 14:33

    • Хотя assert() и удивительно полезен, но там все сделано через C препроцессор.

    • А препроцессор в руках идиота -- Трагедия!

    Так вот. Все новомодные языки прекрасно знают, с кем имеют дело. И препроцессор велено не пущать... https://ders.by/arch/debrel/debrel.html


  1. orignal
    28.07.2025 14:33

    Зачем нужны все эти нагромождения макросов в современном C++, если там есть развитая система шаблонов?


    1. NeoCode
      28.07.2025 14:33

      ... Если там есть все эти нагромождения шаблонов :)


    1. Ivserov
      28.07.2025 14:33

      Удобно для сериализации и логов


    1. haqreu
      28.07.2025 14:33

      Даже в современном C++ кросс-платформенные приложения не могут обойтись без макросов :(


  1. haqreu
    28.07.2025 14:33

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

    А вообще https://github.com/Hirrolot/metalang99 :)


  1. DrMefistO
    28.07.2025 14:33

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


    1. haqreu
      28.07.2025 14:33

      В этой статье вряд ли что-то может пригодиться в реальной жизни, а вот какой-нибудь #pragma omp - это очень, очень полезная препроцессорная магия.


    1. Kelbon
      28.07.2025 14:33

      макросы можно просто раскрыть и посмотреть что там написано, что там отлаживать?)


      1. haqreu
        28.07.2025 14:33

        А, ну да. Игрушечный пример из жизни. Вот решил я как-то матрицу на вектор умножить в Eigen3: auto B = A*x; , где A - матрица, x - вектор. Работало всё как надо, разве что, тормозня несусветная была. Заменил auto на настоящий тип Eigen::VectorXd B = A*x; - и всё взлетело.

        А если auto развернуть...

        барабанная дробь
        Eigen::Product<Eigen::CwiseBinaryOp<Eigen::internal::scalar_quotient_op<double, double>, Eigen::Product<Eigen::Product<Eigen::SparseMatrix<double, 1, int>, Eigen::Transpose<Eigen::SparseMatrix<double, 1, int> >, 2>, Eigen::Transpose<Eigen::Matrix<double, -1, -1, 0, -1, -1> const>, 0> const, Eigen::CwiseNullaryOp<Eigen::internal::scalar_constant_op<double>, Eigen::Matrix<double, -1, -1, 1, -1, -1> const> const>, Eigen::CwiseBinaryOp<Eigen::internal::scalar_quotient_op<double, double>, Eigen::CwiseBinaryOp<Eigen::internal::scalar_difference_op<double, double>, Eigen::CwiseBinaryOp<Eigen::internal::scalar_product_op<double, double>, Eigen::CwiseNullaryOp<Eigen::internal::scalar_constant_op<double>, Eigen::Matrix<double, -1, 1, 0, -1, 1> const> const, Eigen::Product<Eigen::Product<Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::SparseMatrix<double, 1, int>, 0>, Eigen::Matrix<double, -1, 1, 0, -1, 1>, 0> const> const, Eigen::CwiseBinaryOp<Eigen::internal::scalar_product_op<double, double>, Eigen::CwiseNullaryOp<Eigen::internal::scalar_constant_op<double>, Eigen::Matrix<double, -1, 1, 0, -1, 1> const> const, Eigen::Product<Eigen::CwiseUnaryOp<Eigen::internal::scalar_opposite_op<double>, Eigen::Matrix<double, -1, -1, 0, -1, -1> const>, Eigen::Product<Eigen::Product<Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::SparseMatrix<double, 1, int>, 0>, Eigen::Matrix<double, -1, 1, 0, -1, 1>, 0>, 0> const> const> const, Eigen::CwiseNullaryOp<Eigen::internal::scalar_constant_op<double>, Eigen::Matrix<double, -1, 1, 0, -1, 1> const> const>, 0>
        

        Думаете, макросы нагляднее читаются? Я, наверное, старый, но ошибки в MFC/Afx мне до сих пор в кошмарах снятся.


      1. DrMefistO
        28.07.2025 14:33

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