4、CMake条件与循环:if/elseif/else 逻辑判断、foreach() 循环遍历列表、while() 循环、函数与宏(function/macro)
说实话,CMake 这门「语言」刚上手时,很多人觉得它就是个配置工具。但真正把项目做复杂了,你会发现——没有条件判断和循环,你连个像样的跨平台构建都写不出来。今天我们就来聊聊 CMake 里的控制流,以及我这些年踩过的坑。
4.1 条件判断:if/elseif/else
条件判断是 CMake 里最基础的控制结构。说白了,就是根据不同的平台、编译器、选项,走不同的构建路径。
4.1.1 基本语法
if(condition)
# 条件为真时执行
elseif(another_condition)
# 另一个条件
else()
# 都不满足时
endif()
嗯,这里要注意:endif() 后面可以加注释,比如 endif(),但我不建议你写太花哨的东西,保持干净就好。
4.1.2 常用条件表达式
| 表达式 | 含义 | 示例 |
|---|---|---|
WIN32 |
Windows 平台 | if(WIN32) |
APPLE |
macOS / iOS | if(APPLE) |
UNIX |
类 Unix 系统 | if(UNIX) |
MSVC |
Visual Studio 编译器 | if(MSVC) |
CMAKE_CXX_COMPILER_ID STREQUAL "Clang" |
编译器 ID 比较 | if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") |
DEFINED VAR |
变量是否已定义 | if(DEFINED MY_VAR) |
EXISTS file |
文件或目录是否存在 | if(EXISTS "${CMAKE_SOURCE_DIR}/config.h") |
if(WIN32),再写 elseif(APPLE),最后 else() 留给 Linux。这样逻辑清晰,不容易漏掉。
4.1.3 逻辑运算符
if(condition1 AND condition2)
if(condition1 OR condition2)
if(NOT condition)
举个例子,我在项目中遇到过需要同时判断平台和编译器的情况:
if(WIN32 AND MSVC)
message(STATUS "Windows + MSVC,开启特定优化")
add_compile_options(/O2 /GL)
elseif(WIN32 AND MINGW)
message(STATUS "Windows + MinGW,使用 GCC 风格")
add_compile_options(-O2)
endif()
if() 里直接写 if(${VAR}),结果 VAR 是空字符串时,CMake 会报错。正确做法是 if(VAR) 或者 if(DEFINED VAR)。记住:CMake 的 if 里,变量名不用加 ${}。
4.2 foreach() 循环遍历列表
foreach 是我用得最多的循环。你想想看,CMake 里到处都是列表——源文件列表、库列表、子目录列表……没有 foreach,你只能一个个手写,那得多累。
4.2.1 基本用法
set(SOURCES
src/main.cpp
src/utils.cpp
src/network.cpp
)
foreach(SRC ${SOURCES})
message(STATUS "处理源文件: ${SRC}")
# 可以在这里做更多操作,比如设置编译选项
endforeach()
4.2.2 带索引的遍历
有时候你需要知道当前是第几个元素。CMake 3.17 之后支持 foreach() 带 RANGE 和 ZIP_LISTS,但我个人习惯用老办法:
set(INDEX 0)
foreach(SRC ${SOURCES})
math(EXPR INDEX "${INDEX} + 1")
message(STATUS "第 ${INDEX} 个源文件: ${SRC}")
endforeach()
4.2.3 遍历多个列表
我记得有一次需要同时遍历源文件和对应的头文件目录,我是这么写的:
set(SOURCES src/a.cpp src/b.cpp)
set(HEADERS include/a.h include/b.h)
list(LENGTH SOURCES LEN)
math(EXPR MAX_IDX "${LEN} - 1")
foreach(IDX RANGE ${MAX_IDX})
list(GET SOURCES ${IDX} SRC)
list(GET HEADERS ${IDX} HDR)
message(STATUS "配对: ${SRC} <-> ${HDR}")
endforeach()
RANGE 模式支持 RANGE start stop [step],比如 foreach(i RANGE 1 10 2) 会得到 1,3,5,7,9。
4.3 while() 循环
while 循环在 CMake 里用得相对少一些,但某些场景下它比 foreach 更灵活。比如你不知道循环次数,需要根据条件动态退出。
4.3.1 基本语法
set(COUNTER 0)
while(COUNTER LESS 5)
message(STATUS "计数器: ${COUNTER}")
math(EXPR COUNTER "${COUNTER} + 1")
endwhile()
4.3.2 实际应用场景
我在项目中遇到过需要不断查找依赖库的情况。比如某个库可能安装在多个路径下,我需要逐个尝试:
set(SEARCH_PATHS
/usr/local/lib
/opt/lib
/home/user/local/lib
)
set(INDEX 0)
list(LENGTH SEARCH_PATHS TOTAL)
set(FOUND_LIB FALSE)
while(INDEX LESS TOTAL AND NOT FOUND_LIB)
list(GET SEARCH_PATHS ${INDEX} PATH)
if(EXISTS "${PATH}/libfoo.so")
message(STATUS "找到 libfoo.so 在: ${PATH}")
set(FOUND_LIB TRUE)
endif()
math(EXPR INDEX "${INDEX} + 1")
endwhile()
4.4 函数与宏(function/macro)
函数和宏是 CMake 里复用代码的核心手段。很多人分不清它们的区别,其实很简单:
- function:创建新的作用域,变量修改不影响外部
- macro:在调用者作用域内展开,变量修改会影响外部
4.4.1 定义函数
function(add_my_library TARGET_NAME SOURCES)
add_library(${TARGET_NAME} STATIC ${SOURCES})
target_include_directories(${TARGET_NAME}
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include
)
target_compile_options(${TARGET_NAME}
PRIVATE -Wall -Wextra
)
message(STATUS "已创建库: ${TARGET_NAME}")
endfunction()
# 调用
add_my_library(mylib "src/a.cpp;src/b.cpp")
4.4.2 定义宏
macro(print_variable VAR_NAME)
message(STATUS "${VAR_NAME} = ${${VAR_NAME}}")
endmacro()
set(MY_VAR "hello")
print_variable(MY_VAR) # 输出: MY_VAR = hello
4.4.3 函数 vs 宏:关键区别
| 特性 | function | macro |
|---|---|---|
| 作用域 | 独立作用域 | 调用者作用域 |
| 变量修改 | 不影响外部 | 影响外部 |
| 返回值 | 通过 set(PARENT_SCOPE) |
直接修改外部变量 |
| 参数传递 | 按值传递 | 按文本替换 |
| 推荐场景 | 大多数情况 | 简单文本替换 |
4.4.4 函数返回值技巧
函数不能直接返回值,但可以通过 set(... PARENT_SCOPE) 把变量传递到上层:
function(get_platform_suffix RESULT_VAR)
if(WIN32)
set(${RESULT_VAR} "_win" PARENT_SCOPE)
elseif(APPLE)
set(${RESULT_VAR} "_mac" PARENT_SCOPE)
else()
set(${RESULT_VAR} "_linux" PARENT_SCOPE)
endif()
endfunction()
get_platform_suffix(SUFFIX)
message(STATUS "平台后缀: ${SUFFIX}")
4.5 知识体系图
下面这张图帮你理清 CMake 条件与循环的整体脉络:
4.6 实战经验总结
最后分享几个我这些年积累的小经验:
- 条件判断优先用平台变量:
WIN32、APPLE、UNIX这些是 CMake 内置的,比你自己写if(CMAKE_SYSTEM_NAME STREQUAL "Windows")更简洁、更可靠。 - foreach 比 while 更安全:除非你真的需要动态退出条件,否则用 foreach 遍历列表,不容易出 bug。
- 函数参数用大写:我个人习惯函数参数用大写命名,比如
TARGET_NAME、SOURCES,这样一眼就能看出是参数,不是普通变量。 - 宏里慎用
return():宏里的return()行为跟函数不一样,它不会退出宏,而是退出整个 CMake 处理。我吃过这个亏,后来就很少用宏了。
message(STATUS "...") 打印变量值。我经常在 if 分支里加 message(STATUS "进入 WIN32 分支") 这样的日志,方便追踪构建流程。
好了,CMake 的条件与循环就聊到这里。这些基础控制结构,加上之前学的变量和列表操作,已经能应付大部分跨平台构建场景了。记住:写 CMake 跟写代码一样,清晰比炫技更重要。