流程控制:让CMake拥有“判断力”

各位好,欢迎来到第七章。说实话,CMake 如果只会一条道走到黑地执行命令,那它跟批处理脚本也没什么区别。真正让 CMake 变得灵活、智能的,就是流程控制。今天我们就来聊聊 if/elseif/elsewhileforeach 以及 break/continue 这些家伙。

我个人习惯把流程控制比作“CMake 的大脑”。没有它,你的构建脚本就是一根筋;有了它,才能根据不同的平台、编译器、选项做出不同的反应。

CMake 流程控制核心结构 条件分支 if / elseif / else / endif 循环 while / foreach 循环控制 break / continue 变量判断 字符串比较 列表遍历 条件循环 跳出循环 跳过本轮 核心原则 条件决定方向,循环处理重复,控制指令精细调优

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 时,试着用上这些命令,你会发现构建脚本变得灵活多了。


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