5. 控制流:if()/elseif()/else()/endif()、while()、foreach()、break()与continue()
CMake 不是一门通用编程语言,但它确实有自己的控制流。说白了,就是让构建脚本能「做决策」和「重复干活」。我刚开始用 CMake 时,总觉得这些语法是摆设——直到我接手了一个需要跨平台编译、还要根据编译器版本选择不同优化策略的项目。
嗯,那时候我才意识到:没有控制流的 CMakeLists.txt,就像没有舵的船。
5.1 if()/elseif()/else()/endif():条件判断
条件判断是 CMake 里最常用的控制结构。它的基本语法长这样:
if(<condition>)
# 条件为真时执行
elseif(<condition2>)
# 条件2为真时执行
else()
# 以上都不满足时执行
endif()
注意,elseif 和 else 后面必须带括号。我见过不少新手写成 else if(中间加空格),结果 CMake 直接报错。
5.1.1 条件表达式
CMake 的条件判断支持多种表达式类型:
| 表达式类型 | 示例 | 说明 |
|---|---|---|
| 常量 | if(TRUE) / if(FALSE) |
直接判断真假 |
| 变量 | if(DEFINED VAR) |
检查变量是否被定义 |
| 字符串比较 | if("${VAR}" STREQUAL "value") |
区分大小写 |
| 数值比较 | if(VAR GREATER 10) |
支持 EQUAL、LESS、GREATER 等 |
| 逻辑运算 | if(COND1 AND COND2) |
AND、OR、NOT |
| 文件判断 | if(EXISTS "/path/to/file") |
检查文件或目录是否存在 |
if("${MY_VAR}" STREQUAL "debug")。这样可以避免变量为空时语法错误。
5.1.2 实际项目中的用法
我在项目中经常用条件判断来做平台适配:
if(WIN32)
message(STATUS "Windows 平台,启用 Windows 特定编译选项")
add_compile_definitions(WIN32_LEAN_AND_MEAN)
elseif(APPLE)
message(STATUS "macOS 平台,启用 Objective-C 支持")
enable_language(OBJC)
elseif(UNIX)
message(STATUS "Linux/Unix 平台,启用 POSIX 特性")
add_compile_options(-pthread)
else()
message(WARNING "未知平台,请手动检查编译选项")
endif()
你想想看,如果没有条件判断,你得写多少个不同的 CMakeLists.txt?
5.2 while() 循环
while() 循环在 CMake 里用得不多,但某些场景下很实用。它的语法很简单:
set(COUNTER 0)
while(COUNTER LESS 10)
message("计数: ${COUNTER}")
math(EXPR COUNTER "${COUNTER} + 1")
endwhile()
while() 没有内置的递增操作,你得自己用 math(EXPR ...) 或者 string(APPEND ...) 来更新循环变量。我曾经见过有人忘记更新变量,结果陷入死循环,CMake 卡了整整 10 分钟才超时退出。
说实话,我很少在 CMake 里用 while()。大多数情况下,foreach() 就够用了。但如果你需要「直到某个条件满足才停止」的逻辑,while() 就是唯一的选择。
5.3 foreach() 循环
foreach() 是 CMake 里最常用的循环结构。它有好几种变体,我一个个说。
5.3.1 遍历列表
set(SOURCES main.cpp utils.cpp network.cpp)
foreach(FILE ${SOURCES})
message("处理源文件: ${FILE}")
endforeach()
这是最基础的用法。注意,foreach() 的循环变量(这里是 FILE)不需要提前声明,CMake 会自动创建。
5.3.2 遍历范围
foreach(I RANGE 1 5)
message("迭代次数: ${I}")
endforeach()
# 输出: 1 2 3 4 5
你也可以指定步长:
foreach(I RANGE 0 10 2)
message("偶数: ${I}")
endforeach()
# 输出: 0 2 4 6 8 10
5.3.3 遍历 ZIP 列表
CMake 3.17 引入了 ZIP 遍历,可以同时遍历多个列表:
set(NAMES Alice Bob Charlie)
set(AGES 25 30 35)
foreach(NAME AGE IN ZIP_LISTS NAMES AGES)
message("${NAME} 今年 ${AGE} 岁")
endforeach()
这个特性我特别喜欢。以前要同时遍历两个列表,得用索引,麻烦得很。
5.4 break() 与 continue()
这两个命令用来控制循环的执行流程:
break():立即退出当前循环continue():跳过本次循环的剩余部分,进入下一次迭代
看个例子:
foreach(I RANGE 1 10)
if(I EQUAL 3)
continue() # 跳过 3
endif()
if(I GREATER 7)
break() # 大于 7 就退出
endif()
message("当前值: ${I}")
endforeach()
# 输出: 1 2 4 5 6 7
foreach() 里用 break() 退出循环,结果发现外层还有一个 while() 循环。CMake 的 break() 和 continue() 只作用于最内层的循环。如果你想跳出多层循环,得自己用标志变量控制。
5.5 知识体系图
下面这张图总结了 CMake 控制流的整体结构:
5.6 实际项目中的组合用法
最后,我分享一个我在真实项目中用过的模式。当时需要根据不同的编译配置,生成不同的源文件列表:
# 收集所有源文件
set(ALL_SOURCES
src/core.cpp
src/utils.cpp
src/io.cpp
)
# 根据平台和配置过滤
set(FINAL_SOURCES "")
foreach(SRC ${ALL_SOURCES})
# 跳过 Windows 平台不需要的文件
if(WIN32 AND "${SRC}" MATCHES "posix")
continue()
endif()
# 调试模式下跳过优化文件
if(CMAKE_BUILD_TYPE STREQUAL "Debug" AND "${SRC}" MATCHES "optimized")
continue()
endif()
list(APPEND FINAL_SOURCES "${SRC}")
endforeach()
# 检查是否还有源文件
if(NOT FINAL_SOURCES)
message(FATAL_ERROR "没有可编译的源文件!")
endif()
你看,foreach()、continue()、if() 配合使用,就能写出很灵活的构建逻辑。嗯,这就是 CMake 控制流的魅力——它让构建脚本不再是死板的指令列表,而是能根据环境自动调整的智能系统。
公众号:蓝海资料掘金营,微信deep3321