流程控制:让CMake拥有“判断力”
各位好,欢迎来到第七章。说实话,CMake 如果只会一条道走到黑地执行命令,那它跟批处理脚本也没什么区别。真正让 CMake 变得灵活、智能的,就是流程控制。今天我们就来聊聊 if/elseif/else、while、foreach 以及 break/continue 这些家伙。
我个人习惯把流程控制比作“CMake 的大脑”。没有它,你的构建脚本就是一根筋;有了它,才能根据不同的平台、编译器、选项做出不同的反应。
if/elseif/else:给构建脚本装上“红绿灯”
先说说 if 命令。它的基本语法长这样:
if(<condition>)
# 条件为真时执行
elseif(<condition2>)
# 条件2为真时执行
else()
# 以上都不满足时执行
endif()
嗯,这里要注意:endif() 后面的括号不能省。我刚开始学 CMake 时,经常写成 endif 不带括号,结果 CMake 直接报错。后来养成了习惯,所有块结束命令都带括号。
条件判断支持很多种形式。我挑几个最常用的说说:
| 条件类型 | 示例 | 说明 |
|---|---|---|
| 常量判断 | if(TRUE) |
ON、YES、TRUE、1 都视为真 |
| 变量判断 | if(DEFINED VAR) |
检查变量是否被定义 |
| 字符串比较 | if("${VAR}" STREQUAL "hello") |
区分大小写 |
| 数字比较 | if(${COUNT} GREATER 10) |
支持 EQUAL、LESS、GREATER 等 |
| 逻辑运算 | if(A AND B) |
AND、OR、NOT |
避坑指南:我曾经在比较字符串时忘了加引号,写成 if(${VAR} STREQUAL hello)。如果 VAR 的值是空字符串,CMake 会直接报语法错误。所以,比较字符串时一定要加引号:if("${VAR}" STREQUAL "hello")。
来看一个实际例子。假设我们要根据编译器类型设置不同的编译选项:
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
message("检测到 GCC 编译器")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra")
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
message("检测到 Clang 编译器")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Weverything")
elseif(MSVC)
message("检测到 MSVC 编译器")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4")
else()
message(WARNING "未知编译器,使用默认选项")
endif()
你想想看,如果没有 if,你得写三个不同的 CMakeLists.txt 来应对不同编译器,那得多麻烦?
while 循环:条件满足就一直干
while 循环的用法很直接:
set(COUNTER 0)
while(COUNTER LESS 5)
message("当前计数: ${COUNTER}")
math(EXPR COUNTER "${COUNTER} + 1")
endwhile()
这里要注意,CMake 没有自增运算符,所以得用 math(EXPR ...) 来做算术运算。我个人觉得这有点啰嗦,但习惯了也还好。
警告:while 循环一定要确保条件最终会变为假,否则就是死循环。我曾经在调试时忘记更新循环变量,结果 CMake 卡住不动了,最后只能强制杀掉进程。
foreach 循环:遍历列表的利器
foreach 是我在项目中用得最多的循环命令。它有好几种变体,咱们一个一个看。
基本用法:遍历列表
set(SOURCES main.cpp utils.cpp network.cpp)
foreach(SRC ${SOURCES})
message("处理源文件: ${SRC}")
endforeach()
带索引遍历
set(FRUITS apple banana cherry)
foreach(FRUIT RANGE 0 2)
list(GET FRUITS ${FRUIT} ITEM)
message("索引 ${FRUIT}: ${ITEM}")
endforeach()
范围遍历
foreach(N RANGE 1 10 2) # 从1到10,步长为2
message("奇数: ${N}")
endforeach()
说实话,RANGE 的步长参数很容易被忽略。我第一次用的时候,以为 RANGE 1 10 2 是“从1到10,共2个”,结果打印出来是 1、3、5、7、9。后来看了文档才明白,第三个参数是步长。
小技巧:如果你想遍历一个列表,但又想同时获取索引和值,可以这样写:
set(ITEMS a b c)
list(LENGTH ITEMS LEN)
math(EXPR MAX_IDX "${LEN} - 1")
foreach(IDX RANGE ${MAX_IDX})
list(GET ITEMS ${IDX} VAL)
message("${IDX}: ${VAL}")
endforeach()
break 和 continue:循环的“刹车”与“跳过”
这两个命令的作用跟其他语言一样:break 立即退出循环,continue 跳过本轮剩余代码,进入下一轮。
来看个例子:
set(NUMBERS 1 2 3 4 5 6 7 8 9 10)
foreach(N ${NUMBERS})
if(N EQUAL 5)
message("遇到5,跳过")
continue()
endif()
if(N GREATER 8)
message("大于8,退出循环")
break()
endif()
message("处理数字: ${N}")
endforeach()
输出结果会是:
处理数字: 1
处理数字: 2
处理数字: 3
处理数字: 4
遇到5,跳过
处理数字: 6
处理数字: 7
处理数字: 8
大于8,退出循环
注意看,5 被跳过了,9 和 10 根本没处理,因为遇到 9 时 break() 直接退出了循环。
个人经验:我在处理一个大型项目的源文件列表时,曾经用 break 来实现“只编译前 N 个文件用于测试”的功能。在循环里加一个计数器,达到目标数量后直接 break(),比用条件判断包裹整个循环体要清爽得多。
综合实战:一个跨平台构建的流程控制示例
最后,咱们把今天学的知识串起来,写一个稍微复杂点的例子:
# 根据平台和构建类型设置不同的源文件
set(BASE_SRCS main.cpp core.cpp)
if(WIN32)
message("Windows 平台,添加 Windows 专属文件")
set(PLATFORM_SRCS win32_api.cpp)
elseif(APPLE)
message("macOS 平台,添加 macOS 专属文件")
set(PLATFORM_SRCS mac_utils.mm)
elseif(UNIX)
message("Linux/Unix 平台,添加 Unix 专属文件")
set(PLATFORM_SRCS linux_utils.cpp)
else()
message(FATAL_ERROR "不支持的平台")
endif()
# 遍历所有源文件,排除测试文件
set(ALL_SRCS ${BASE_SRCS} ${PLATFORM_SRCS})
set(FINAL_SRCS)
foreach(SRC ${ALL_SRCS})
if("${SRC}" MATCHES "_test\\.")
message("跳过测试文件: ${SRC}")
continue()
endif()
list(APPEND FINAL_SRCS ${SRC})
endforeach()
message("最终参与编译的源文件: ${FINAL_SRCS}")
这个例子用到了 if/elseif/else 做平台判断,用 foreach 遍历文件列表,用 continue 跳过测试文件。你看,流程控制组合起来用,威力就出来了。
好了,关于流程控制的内容就讲到这里。记住一句话:条件决定方向,循环处理重复,控制指令帮你精细调优。下次写 CMakeLists.txt 时,试着用上这些命令,你会发现构建脚本变得灵活多了。