5、CMake流程控制:if-elseif-else条件判断、foreach循环、while循环、break与continue
流程控制,说白了就是让CMake脚本学会「动脑子」。
我刚开始用CMake时,觉得它就是个配置工具,写点变量、加几个文件就完事了。后来项目越来越复杂,不同平台、不同编译器、不同选项……嗯,没有流程控制,那脚本根本没法看。
这一章,我们就来聊聊CMake里的条件判断和循环。这些东西看着简单,但用好了,能让你的CMakeLists.txt既干净又灵活。
5.1 if-elseif-else:让CMake学会做选择
条件判断,是流程控制的基础。CMake的if()语法,跟其他语言差不多,但有几个坑,我当年踩过。
基本语法
if(condition)
# 条件为真时执行
elseif(another_condition)
# 另一个条件为真时执行
else()
# 以上都不满足时执行
endif()
注意,endif()后面要加括号。我见过有人写成endif,结果CMake直接报错。这种小细节,容易让人抓狂。
常用条件表达式
| 表达式 | 说明 | 示例 |
|---|---|---|
if(var) |
变量非空、非0、非OFF、非NO等 | if(WIN32) |
if(NOT var) |
变量为空或假值 | if(NOT MSVC) |
if(var1 STREQUAL var2) |
字符串相等 | if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") |
if(var1 GREATER var2) |
数值比较(大于) | if(CMAKE_VERSION VERSION_GREATER 3.20) |
if(DEFINED var) |
变量是否被定义 | if(DEFINED MY_OPTION) |
if(EXISTS path) |
文件或目录是否存在 | if(EXISTS "${CMAKE_SOURCE_DIR}/config.h.in") |
避坑指南:我曾经在if()里直接写if(${VAR}),结果VAR是空字符串时,CMake会报语法错误。正确写法是if(VAR),不要加${}。除非你要比较两个变量的值,比如if(${A} STREQUAL ${B})。
实际案例:跨平台编译选项
if(WIN32)
message(STATUS "Windows平台,启用Windows特有优化")
add_definitions(-DWIN32_LEAN_AND_MEAN)
elseif(APPLE)
message(STATUS "macOS平台,启用Clang特有标志")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++")
elseif(UNIX)
message(STATUS "Linux平台,启用GCC特有标志")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread")
else()
message(WARNING "未知平台,使用默认配置")
endif()
我个人习惯把平台判断放在项目开头,这样后面所有配置都能基于这个判断来做。你想想看,如果每个子目录都写一遍if(WIN32),那得多乱。
5.2 foreach循环:遍历列表的好帮手
foreach()是我在CMake里用得最多的循环。遍历源文件、遍历子目录、遍历依赖库……几乎天天用。
基本语法
foreach(loop_var arg1 arg2 ...)
# 循环体
endforeach()
遍历列表
set(SOURCES main.cpp utils.cpp network.cpp)
foreach(src ${SOURCES})
message(STATUS "添加源文件: ${src}")
target_sources(myapp PRIVATE ${src})
endforeach()
嗯,这里要注意,foreach()里的loop_var是循环变量,每次迭代会被赋值为列表中的一个元素。如果你在循环里修改了loop_var,不会影响原列表。
遍历数字范围
foreach(i RANGE 1 10)
message(STATUS "第${i}次迭代")
endforeach()
这个RANGE关键字,可以指定起始、结束和步长。比如foreach(i RANGE 0 10 2),就是0、2、4、6、8、10。我在项目中用它来生成一组测试用例的编号。
遍历字典(ZIP模式)
set(NAMES Alice Bob Charlie)
set(AGES 25 30 35)
foreach(name age IN ZIP_LISTS NAMES AGES)
message(STATUS "${name} 年龄: ${age}")
endforeach()
这个功能是CMake 3.17才加入的。说实话,我一开始觉得没啥用,后来做配置映射时发现真香。比如把源文件列表和对应的编译选项列表一一对应起来。
小技巧:如果你要遍历一个目录下的所有源文件,可以用file(GLOB)配合foreach。但我建议你显式列出源文件,GLOB的隐式行为容易导致构建系统漏掉新文件。
5.3 while循环:条件驱动的循环
while()循环,说白了就是「只要条件满足,就一直执行」。用得比foreach少,但在某些场景下很管用。
基本语法
while(condition)
# 循环体
endwhile()
示例:计数器循环
set(counter 0)
while(counter LESS 5)
message(STATUS "计数器: ${counter}")
math(EXPR counter "${counter} + 1")
endwhile()
注意,CMake没有counter++这种自增运算符,得用math(EXPR ...)来做算术运算。我第一次写的时候,找了半天没找到++,后来才发现CMake就是这么「朴实无华」。
实际案例:等待文件生成
set(wait_count 0)
while(NOT EXISTS "${CMAKE_CURRENT_BINARY_DIR}/generated.h")
math(EXPR wait_count "${wait_count} + 1")
if(wait_count GREATER 10)
message(FATAL_ERROR "等待超时,文件未生成")
endif()
execute_process(COMMAND ${CMAKE_COMMAND} -E sleep 1)
endwhile()
message(STATUS "文件已生成")
这个例子,我在做代码生成器时用过。生成器在构建过程中生成头文件,主项目需要等它生成完才能继续。用while循环加超时判断,既简单又可靠。
警告:while循环一定要有退出条件,否则会变成死循环。CMake不像编程语言那样有调试器可以中断,死循环只能强制杀掉进程。我曾经在CI上跑了一个死循环,把构建节点卡了半小时……嗯,从那以后我写while都会加一个计数器做保险。
5.4 break与continue:循环的控制杆
break和continue,是循环里的「刹车」和「跳过」。CMake 3.2之后才支持这两个命令,之前的老版本只能用if嵌套来模拟。
break:跳出循环
foreach(file ${ALL_FILES})
if(file STREQUAL "stop.txt")
message(STATUS "遇到stop.txt,停止处理")
break()
endif()
message(STATUS "处理文件: ${file}")
endforeach()
break()会立即终止当前循环,不管后面还有多少元素。我在做文件批处理时,经常用这个来提前退出。
continue:跳过本次迭代
foreach(file ${SOURCE_FILES})
if(file MATCHES "_test\\.cpp$")
message(STATUS "跳过测试文件: ${file}")
continue()
endif()
target_sources(myapp PRIVATE ${file})
endforeach()
continue()会跳过当前迭代的剩余部分,直接进入下一次迭代。说白了就是「这个我不要了,下一个」。
嵌套循环中的使用
foreach(outer ${LIST_A})
foreach(inner ${LIST_B})
if(inner STREQUAL "skip")
continue() # 只跳过内层循环的当前迭代
endif()
if(outer STREQUAL "stop")
break() # 只跳出内层循环
endif()
message(STATUS "${outer} - ${inner}")
endforeach()
endforeach()
注意,break()和continue()只作用于它们所在的最近一层循环。如果你想跳出多层循环,得自己加标志变量。这个限制,说实话有点烦,但CMake就是这么设计的。
避坑指南:我曾经在foreach里用break(),结果发现它跳出了foreach,但没跳出外层的while。后来我加了一个set(done TRUE),然后在每个循环条件里都检查这个变量。虽然啰嗦了点,但管用。
5.5 知识体系总览
下面这张图,把本章的知识点串起来了。你可以把它当作一个快速参考。
5.6 总结
流程控制,是CMake从「配置脚本」走向「构建程序」的关键一步。
我个人觉得,if-elseif-else是日常用得最多的,几乎每个项目都会用到。foreach次之,处理源文件列表时特别好用。while用得少,但用对了能解决一些棘手问题。break和continue,算是锦上添花,让循环更灵活。
你想想看,如果没有这些流程控制,你的CMakeLists.txt会变成什么样?大概就是一堆硬编码的变量,换个平台就得改半天。有了流程控制,一份脚本走天下,这才是CMake该有的样子。
最后提醒一句:写流程控制时,多想想「这个条件会不会永远不满足?」、「这个循环会不会永远不结束?」。多留个心眼,少踩几个坑。
公众号:蓝海资料掘金营,微信deep3321