6、循环与函数:foreach循环、while循环、function()与macro()定义、参数传递机制
说实话,CMake 的循环和函数,是我早期做项目时踩坑最多的地方。你想想看,一个构建系统,如果连循环和函数都写不明白,那项目一复杂起来,CMakeLists.txt 就会变成一团乱麻。我个人习惯是把这些控制结构当作「脚本语言」来用,但 CMake 又有它自己的一套脾气。
这一章,我们就来彻底搞懂 foreach、while、function 和 macro。我会把我在实际项目中遇到的坑,一个一个指给你看。
6.1 foreach 循环:遍历列表的利器
foreach 是我用得最多的循环。说白了,就是遍历一个列表,对每个元素做点事。
基本语法
foreach(变量名 列表元素1 列表元素2 ...)
# 循环体
endforeach()
举个例子:
set(SOURCES main.cpp utils.cpp network.cpp)
foreach(SRC ${SOURCES})
message("当前源文件: ${SRC}")
endforeach()
输出:
当前源文件: main.cpp
当前源文件: utils.cpp
当前源文件: network.cpp
SRC,这样一眼就能看出是循环变量。当然,这只是个人风格,你用 src 也行。
遍历列表的几种变体
foreach 有好几种遍历方式,我挑最常用的说:
| 语法 | 说明 | 示例 |
|---|---|---|
foreach(var IN LISTS listVar) |
遍历一个列表变量 | foreach(SRC IN LISTS SOURCES) |
foreach(var IN ITEMS item1 item2) |
遍历直接给出的项 | foreach(SRC IN ITEMS a.cpp b.cpp) |
foreach(var RANGE start stop [step]) |
遍历数字范围 | foreach(i RANGE 1 10) |
foreach(var ZIP_LISTS list1 list2) |
同时遍历多个列表(CMake 3.17+) | foreach(a b IN ZIP_LISTS L1 L2) |
我曾经在项目中需要同时处理源文件和对应的头文件,ZIP_LISTS 就派上了大用场:
set(SOURCES main.cpp utils.cpp)
set(HEADERS main.h utils.h)
foreach(SRC HDR IN ZIP_LISTS SOURCES HEADERS)
message("源文件: ${SRC},头文件: ${HDR}")
endforeach()
6.2 while 循环:条件驱动
while 循环我用的相对少一些,但在某些场景下,比如等待某个条件成立,它就很合适。
set(COUNTER 0)
while(COUNTER LESS 5)
message("计数: ${COUNTER}")
math(EXPR COUNTER "${COUNTER} + 1")
endwhile()
break 和 continue 语句(至少在 3.x 版本中)。所以,如果你需要提前退出循环,得用 if() 配合 return() 或者 set() 来变通。我曾经因为这个踩过坑,写了个死循环,CMake 跑了半天没反应。
6.3 function() 与 macro():定义你的构建逻辑
function 和 macro 是 CMake 中最重要的抽象手段。它们都能封装一段逻辑,但行为有本质区别。
function():真正的函数
function 会创建一个新的作用域。也就是说,函数内部定义的变量,默认不会影响外部。
function(add_sources target)
set(SOURCES main.cpp utils.cpp)
target_sources(${target} PRIVATE ${SOURCES})
message("函数内部: SOURCES = ${SOURCES}")
endfunction()
set(SOURCES old.cpp)
add_sources(myapp)
message("函数外部: SOURCES = ${SOURCES}")
输出:
函数内部: SOURCES = main.cpp utils.cpp
函数外部: SOURCES = old.cpp
看到了吗?函数内部的 SOURCES 没有污染外部。这就是作用域隔离的好处。
macro():更像文本替换
macro 不会创建新作用域。它本质上就是文本替换——把宏调用处的代码替换成宏体。
macro(add_sources target)
set(SOURCES main.cpp utils.cpp)
target_sources(${target} PRIVATE ${SOURCES})
message("宏内部: SOURCES = ${SOURCES}")
endmacro()
set(SOURCES old.cpp)
add_sources(myapp)
message("宏外部: SOURCES = ${SOURCES}")
输出:
宏内部: SOURCES = main.cpp utils.cpp
宏外部: SOURCES = main.cpp utils.cpp
宏把外部的 SOURCES 给覆盖了!
6.4 参数传递机制
无论是 function 还是 macro,参数传递的机制都一样。CMake 的参数是位置参数,但也有一些高级用法。
基本参数
function(my_func arg1 arg2)
message("arg1 = ${arg1}, arg2 = ${arg2}")
endfunction()
my_func("hello" "world")
可变参数:ARGV 和 ARGN
如果你不知道调用者会传多少个参数,可以用 ARGV(所有参数列表)和 ARGN(超出命名参数的部分)。
function(print_all first)
message("第一个参数: ${first}")
message("所有参数: ${ARGV}")
message("剩余参数: ${ARGN}")
endfunction()
print_all("a" "b" "c" "d")
输出:
第一个参数: a
所有参数: a;b;c;d
剩余参数: b;c;d
关键字参数:cmake_parse_arguments
这是我最喜欢的特性。当函数参数很多时,位置参数会让人崩溃。用关键字参数,调用时一目了然。
include(CMakeParseArguments)
function(create_app)
set(options STATIC SHARED)
set(oneValueArgs NAME OUTPUT_DIR)
set(multiValueArgs SOURCES LIBS)
cmake_parse_arguments(
ARG
"${options}"
"${oneValueArgs}"
"${multiValueArgs}"
${ARGN}
)
if(ARG_STATIC)
add_library(${ARG_NAME} STATIC ${ARG_SOURCES})
elseif(ARG_SHARED)
add_library(${ARG_NAME} SHARED ${ARG_SOURCES})
endif()
target_link_libraries(${ARG_NAME} ${ARG_LIBS})
set_target_properties(${ARG_NAME} PROPERTIES
LIBRARY_OUTPUT_DIRECTORY ${ARG_OUTPUT_DIR}
)
endfunction()
create_app(
NAME mylib
SOURCES lib.cpp utils.cpp
LIBS pthread
SHARED
OUTPUT_DIR "${CMAKE_BINARY_DIR}/lib"
)
你看,调用的时候,每个参数前面都有名字,代码可读性瞬间提升。我在大型项目中,所有自定义函数都强制使用 cmake_parse_arguments,团队协作时几乎不会出错。
cmake_parse_arguments,结果发现 ARGN 的行为在 macro 和 function 中不一样。macro 中 ARGN 包含所有参数(包括命名参数),而 function 中只包含剩余参数。所以,我建议只在 function 中使用 cmake_parse_arguments。
6.5 知识体系图
下面这张图,帮你理清本章的核心脉络:
6.6 总结与建议
嗯,这一章的内容不少,但核心就几点:
- 循环: foreach 是主力,while 是备选。记住 foreach 的几种变体,特别是 ZIP_LISTS,能省不少事。
- 函数 vs 宏: 优先用 function。macro 虽然在某些场景下性能更好(没有作用域创建开销),但副作用太隐蔽。我见过太多因为 macro 导致变量被意外覆盖的 bug。
- 参数传递: 参数多了就用
cmake_parse_arguments,这是专业 CMake 代码的标志。别偷懒写一堆位置参数,三个月后你自己都看不懂。
最后说一句:CMake 的循环和函数,本质上是在「描述构建过程」,而不是「执行构建过程」。理解这一点,你就能写出更清晰、更可维护的构建脚本。