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()

注意,elseifelse 后面必须带括号。我见过不少新手写成 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()
注意:CMake 的 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 控制流的整体结构:

CMake 控制流知识体系 条件判断 循环 循环控制 if() / elseif() / else() / endif() 常量 / 变量 / 字符串 / 数值 逻辑运算:AND / OR / NOT 文件判断:EXISTS / IS_DIRECTORY while() 循环 foreach() 遍历列表 foreach() 遍历范围 foreach() ZIP 遍历 break():立即退出循环 continue():跳过本次迭代 仅作用于最内层循环 核心原则: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 控制流的魅力——它让构建脚本不再是死板的指令列表,而是能根据环境自动调整的智能系统。

个人建议:控制流虽然好用,但别滥用。CMake 的本质是声明式构建系统,过多的条件判断会让 CMakeLists.txt 变得难以维护。我一般只在平台适配、编译选项选择、以及源文件过滤这三个场景使用控制流。

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