Почему аргументы в макросах CMake ведут себя иначе, чем в функциях?
Разбор cmake error: почему if(p) ложно в macro несмотря на p=ON, ARGV1 пусто в макросах, но работает в cmake functions. Анализ scope, переменных, отладка с cmake --trace-expand и рекомендации по использованию macro vs function.
Почему аргументы в макросах 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 ведут себя иначе, чем в функциях
- Анализ примера кода
- Почему if(p) ложно в макросе, несмотря на ON
- Поведение ARGV1: пусто в макросе, x в функции
- Scope и локальные переменные: ключевые различия
- Отладка с cmake --trace-expand и message
- Когда использовать macro, а когда function
- Источники
- Заключение
Почему аргументы в макросах 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. Круто, но обманчиво.
Анализ примера кода
Давайте разберём ваш код построчно. Вот он целиком для ясности:
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-outputvariable_watch(p)— следит за изменениями.
В документации это стандарт для cmake debug.
Когда использовать macro, а когда function
Макросы (cmake macros): для простых скриптов, где нужна подстановка в текущий scope. Минусы: загрязняют пространство, нет return.
Функции (cmake functions): всегда, когда логика сложная, нужны локальные переменные. Плюс: return(), PARENT_SCOPE.
Рекомендация: 90% случаев — functions. Макросы только для утилит вроде include_guard.
Если аргументы — строки для подстановки (шаблоны), macro ок. Иначе — function.
Источники
- macro — CMake 3.28.3 Documentation — Описание работы аргументов в макросах CMake с примерами: https://cmake.org/cmake/help/latest/command/macro.html
- function vs macro in cmake? — Stack Overflow — Сравнение scope, ARGV и лучших практик от разработчиков: https://stackoverflow.com/questions/24297999/function-vs-macro-in-cmake
- 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.
Макросы CMake разворачиваются в текущем scope с строковой подстановкой аргументов, без создания новых переменных — поэтому изменения не сохраняются за пределами, как в случае с arg.
Функции создают локальный контекст, где аргументы — полноценные переменные, видимые только внутри (используйте PARENT_SCOPE для экспорта).
В макросе list(LENGTH) видит строку вместо списка; для списков применяйте ${${my_list}}.
return() в макросе выходит из вызывающей функции, в функции — только из себя.
Для отладки используйте cmake --trace-expand, чтобы увидеть разворот макросов. Это объясняет ложное if(p) и пустой ARGV1.
