Программирование

Почему аргументы в макросах CMake ведут себя иначе, чем в функциях?

Разбор cmake error: почему if(p) ложно в macro несмотря на p=ON, ARGV1 пусто в макросах, но работает в cmake functions. Анализ scope, переменных, отладка с cmake --trace-expand и рекомендации по использованию macro vs function.

3 ответа 1 просмотр

Почему аргументы в макросах CMake ведут себя по-другому по сравнению с функциями?

Пример кода CMake:

cmake
macro(my_macro p)
 message("--my_macro--")
 message("p is: ${p}")
 if(p) # (1)
 message("\"if(p)\" evaluates to true")
 endif()
 set(i 1)
 message("ARGV${i}:${ARGV${i}}") # (2)
endmacro()

function(my_func p)
 message("--my_function--")
 message("p is: ${p}")
 if(p)
 message("\"if(p)\" evaluates to true")
 endif()
 set(i 1)
 message("ARGV${i}:${ARGV${i}}")
endfunction()

my_macro(ON x)
my_func(ON x)

Вывод при выполнении:

--my_macro--
p is ON
ARGV1:

--my_function--
p is ON
"if(p)" evaluates to true
ARGV1:x

Конкретные вопросы:

  • Почему на строке (1) условие if(p) оценивается как false в макросе, хотя p равно ON (что видно из message), и в функции оно истинно?
  • Почему на строке (2) ARGV1 пусто в макросе, а в функции равно x?

В макросах CMake аргументы подставляются как строки до выполнения тела, поэтому if(p) видит p как неопределённую переменную, несмотря на то что message("p is: ${p}") выводит ON — классическая cmake error из-за особенностей cmake macros. В функциях (cmake functions) аргументы захватываются как локальные cmake variables, где if(p) истинно, а ARGV1 получает значение x. Лишние аргументы в макросах попадают в ARGN, а не в ARGV, что объясняет пустоту на строке (2).


Содержание


Почему аргументы в макросах CMake ведут себя иначе, чем в функциях

Представьте: вы пишете CMakeLists.txt, вызываете my_macro(ON x) и ждёте, что всё сработает как в обычной функции. Но нет. if(p) молчит в макросе, хотя ${p} равно ON. А в my_func всё идеально. Почему так?

Всё дело в природе cmake macros и cmake functions. Макросы — это как препроцессор в C: CMake подставляет аргументы как строки в тело макроса до его выполнения. Нет создания новых переменных, нет изоляции. Функции же создают локальный scope, где аргументы — полноценные переменные.

Из официальной документации CMake ясно: “Macro arguments are treated as simple string replacement.” То есть p в if(p) — это не значение ON, а просто символ p, который CMake ищет как переменную. Не нашёл — false.

А в функциях? Там p — локальная cmake variable. if(ON) — это true, потому что CMake знает, как интерпретировать булевы значения вроде ON.

Но подождите, а message("p is: ${p}") почему работает? Потому что ${p} — это подстановка строки на этапе разворачивания. К моменту message уже ясно: p is ON. Круто, но обманчиво.


Анализ примера кода

Давайте разберём ваш код построчно. Вот он целиком для ясности:

cmake
macro(my_macro p)
 message("--my_macro--")
 message("p is: ${p}")
 if(p) # (1)
 message("\"if(p)\" evaluates to true")
 endif()
 set(i 1)
 message("ARGV${i}:${ARGV${i}}") # (2)
endmacro()

function(my_func p)
 message("--my_function--")
 message("p is: ${p}")
 if(p)
 message("\"if(p)\" evaluates to true")
 endif()
 set(i 1)
 message("ARGV${i}:${ARGV${i}}")
endfunction()

my_macro(ON x)
my_func(ON x)

Вывод:

--my_macro--
p is ON
ARGV1:

--my_function--
p is ON
"if(p)" evaluates to true
ARGV1:x

В макросе: ${p} подставляется на ON, но if(p) — false. ARGV1: пусто. В функции: всё ок.

Это не баг, а фича. Макрос разворачивается в текущий scope. Строка (1) становится просто if(p), где p — глобальная (или родительская) переменная. Если её нет — false. Строка (2): после p (ARGV0) идёт x, но в ARGV1 ничего, потому что ARGV — фиксированный массив для именованных позиций, а лишнее — в ARGN.

В функции ARGV работает по-другому: все аргументы нумеруются последовательно.


Почему if(p) ложно в макросе, несмотря на ON

Вот ключевой cmake error: message показывает ON, но if(p) игнорирует. Почему?

Макрос разворачивается до выполнения. CMake видит:

message("--my_macro--")
message("p is: ON") # подстановка произошла
if(p) # p не подставляется! Это буквально "if(p)"
 message("...")
endif()

if(p) ищет переменную p в текущем scope. Нет такой? False. Даже если раньше был set(p ON) — в вашем примере p не существует как переменная.

В функции:

function(my_func p) # p = "ON" как локальная переменная
...
if(p) # if("ON") -> true, потому что p ссылается на локальную "ON"

Проверьте сами: добавьте message("typeof p: ${p}") — нет, лучше if(DEFINED p) перед if. В макросе DEFINED false.

Решение? Используйте ${p} везде: if(${p}). Тогда if(ON) сработает. Но это хрупко — если ${p} пусто, if(“”) тоже false.

Из обсуждений на Stack Overflow: “Macros perform simple textual substitution, functions create new variable scope.”


Поведение ARGV1: пусто в макросе, x в функции

Строка (2): message("ARGV${i}:${ARGV${i}}") с i=1. Вывод в макросе: ARGV1:, в функции: ARGV1:x.

ARGV — это специальный набор переменных: ARGV0, ARGV1… для позиционных аргументов после формальных параметров.

В макросе:

  • Формальный p → ARGV0 = “ON”
  • Лишний xне в ARGV1, а в ARGN (список всех лишних аргументов)

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

В функции:

  • p — формальный, ARGV0 = “ON”
  • x — следующий, ARGV1 = “x”

Да, документация подтверждает: в macro ARGN = все аргументы после формальных, ARGV — подмножество.

Хотите x? message("ARGN: ${ARGN}") в макросе покажет x.


Scope и локальные переменные: ключевые различия

CMake имеет глобальный scope + локальные в функциях. Макросы не создают новый — всё в родительском.

Пример: set(var 42) в макросе изменит глобальную var. В функции — только локальную, если не PARENT_SCOPE.

Это влияет на cmake variables: в macro изменения “протекают” наружу, в function — изолированы.

В вашем случае set(i 1) в macro создаст i в родителе. В function — локально, но поскольку после — видно.

Плюс: функции поддерживают return(), макросы — нет (выход из родителя).

Из Stack Overflow: функции для изоляции, макросы для простых подстановок.


Отладка с cmake --trace-expand и message

Подозреваете cmake error? Запустите cmake --trace-expand yourfile.cmake. Покажет развёрнутый код.

Для макроса увидите:

message("p is: ON")
if(p) # без подстановки!

cmake message — ваш друг: message(STATUS "p defined? ${p} = ${p}"). Или message(FATAL_ERROR) для остановки.

Другие трюки:

  • cmake --debug-output
  • variable_watch(p) — следит за изменениями.

В документации это стандарт для cmake debug.


Когда использовать macro, а когда function

Макросы (cmake macros): для простых скриптов, где нужна подстановка в текущий scope. Минусы: загрязняют пространство, нет return.

Функции (cmake functions): всегда, когда логика сложная, нужны локальные переменные. Плюс: return(), PARENT_SCOPE.

Рекомендация: 90% случаев — functions. Макросы только для утилит вроде include_guard.

Если аргументы — строки для подстановки (шаблоны), macro ок. Иначе — function.


Источники

  1. macro — CMake 3.28.3 Documentation — Описание работы аргументов в макросах CMake с примерами: https://cmake.org/cmake/help/latest/command/macro.html
  2. function vs macro in cmake? — Stack Overflow — Сравнение scope, ARGV и лучших практик от разработчиков: https://stackoverflow.com/questions/24297999/function-vs-macro-in-cmake
  3. macro — Рунебук — Русскоязычное объяснение макросов и аргументов ARGV/ARGN: https://runebook.dev/ru/docs/cmake/command/macro/

Заключение

В cmake macros аргументы — строковая подстановка без scope, что ломает if(p) и ARGV1, в то время как cmake functions дают локальные переменные и предсказуемое поведение. Всегда предпочитайте функции для избежания cmake error, а для отладки — cmake --trace-expand. Теперь ваш код заработает: замените macro на function или используйте ${p} в условиях.

В макросах CMake аргументы обрабатываются как строковые замены, подобно препроцессору C, а не как полноценные переменные.

Поэтому в message("p is: ${p}") значение ${p} подставляется как ON, но в if(p) переменная p трактуется как несуществующая, и условие оценивается как false — типичная ошибка CMake.

В функциях аргументы становятся локальными переменными, где if(p) истинно для значения ON.

ARGV1 в макросе пусто, поскольку после формального параметра p (ARGV0 = ON) дополнительные аргументы попадают в ARGN, а не в ARGV1.

В функциях ARGV содержит полный список: ARGV0 = ON, ARGV1 = x. Это ключевое различие между макросами и функциями CMake.

R

Макросы CMake разворачиваются в текущем scope с строковой подстановкой аргументов, без создания новых переменных — поэтому изменения не сохраняются за пределами, как в случае с arg.

Функции создают локальный контекст, где аргументы — полноценные переменные, видимые только внутри (используйте PARENT_SCOPE для экспорта).

В макросе list(LENGTH) видит строку вместо списка; для списков применяйте ${${my_list}}.

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

Для отладки используйте cmake --trace-expand, чтобы увидеть разворот макросов. Это объясняет ложное if(p) и пустой ARGV1.

Авторы
R
Разработчик
I
Разработчик системного ПО
Y
Разработчик
M
Разработчик
A
Программист
B
Разработчик
Источники
Система сборки ПО
Stack Overflow / Q&A платформа
Q&A платформа
Проверено модерацией
Модерация
Почему аргументы в макросах CMake ведут себя иначе, чем в функциях?