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()
注意: CMake 的 while 循环没有 breakcontinue 语句(至少在 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 给覆盖了!

核心区别: function 有独立作用域,macro 没有。我建议:除非你明确需要修改外部变量,否则一律用 function。macro 容易引发「幽灵变量」问题,调试起来很头疼。

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,团队协作时几乎不会出错。

避坑指南: 我曾经在 macro 中使用 cmake_parse_arguments,结果发现 ARGN 的行为在 macro 和 function 中不一样。macro 中 ARGN 包含所有参数(包括命名参数),而 function 中只包含剩余参数。所以,我建议只在 function 中使用 cmake_parse_arguments

6.5 知识体系图

下面这张图,帮你理清本章的核心脉络:

CMake 循环与函数知识体系 循环 (Loops) foreach while IN LISTS / ITEMS RANGE ZIP_LISTS 条件判断 无 break/continue 函数与宏 (Functions & Macros) function() macro() 独立作用域 推荐使用 文本替换 无作用域 参数传递机制 位置参数 ARGV / ARGN cmake_parse_arguments 关键字参数

6.6 总结与建议

嗯,这一章的内容不少,但核心就几点:

  • 循环: foreach 是主力,while 是备选。记住 foreach 的几种变体,特别是 ZIP_LISTS,能省不少事。
  • 函数 vs 宏: 优先用 function。macro 虽然在某些场景下性能更好(没有作用域创建开销),但副作用太隐蔽。我见过太多因为 macro 导致变量被意外覆盖的 bug。
  • 参数传递: 参数多了就用 cmake_parse_arguments,这是专业 CMake 代码的标志。别偷懒写一堆位置参数,三个月后你自己都看不懂。

最后说一句:CMake 的循环和函数,本质上是在「描述构建过程」,而不是「执行构建过程」。理解这一点,你就能写出更清晰、更可维护的构建脚本。


公众号:蓝海资料掘金营,微信deep3321