8. 流程控制:if条件判断、elseif/else/endif、while循环、foreach循环、break/continue

流程控制,说白了就是让CMake学会「看情况办事」。

我刚开始用CMake时,觉得它就是个配置工具,哪需要什么条件判断?后来项目一复杂,不同平台、不同编译器、不同选项……嗯,没有流程控制,代码根本没法看。

这一节,咱们就把CMake里的判断和循环讲透。你想想看,写CMake和写C++其实挺像的,只是语法更「朴实」一点。

8.1 if条件判断:让CMake学会做决定

if语句是CMake里最常用的流程控制。它的基本结构长这样:

if(<condition>)
  # 条件为真时执行
elseif(<condition2>)
  # 条件2为真时执行
else()
  # 以上都不满足时执行
endif()

注意,这里的括号不是可选的。我见过不少新手写成 if condition 不带括号,CMake直接报错。记住:括号必须写

8.1.1 常用判断条件

CMake的if判断支持多种条件类型,我整理了一张表,方便你查阅:

条件类型 示例 说明
常量判断 if(TRUE) / if(FALSE) TRUE为真,FALSE、OFF、NO、0、空字符串为假
变量判断 if(DEFINED VAR) 判断变量是否被定义
字符串比较 if("${VAR}" STREQUAL "hello") 区分大小写,用STREQUAL
数值比较 if(${NUM} GREATER 10) 支持LESS、GREATER、EQUAL等
文件判断 if(EXISTS "/path/to/file") 判断文件或目录是否存在
逻辑运算 if(A AND B) / if(A OR B) 支持AND、OR、NOT
我的小习惯:判断字符串时,我习惯把变量用双引号包起来,比如 if("${VAR}" STREQUAL "hello")。为什么?因为如果VAR未定义,不加引号会直接展开成空,容易出奇怪的问题。

8.1.2 实战:跨平台判断

我在项目中遇到过最典型的场景,就是根据操作系统设置不同的编译选项:

if(WIN32)
  message(STATUS "当前是Windows系统")
  add_compile_definitions(PLATFORM_WINDOWS)
elseif(APPLE)
  message(STATUS "当前是macOS系统")
  add_compile_definitions(PLATFORM_MACOS)
elseif(UNIX AND NOT APPLE)
  message(STATUS "当前是Linux系统")
  add_compile_definitions(PLATFORM_LINUX)
else()
  message(WARNING "未知操作系统,使用默认配置")
endif()

这里WIN32、APPLE、UNIX都是CMake内置的变量,你直接拿来用就行。注意顺序——先判断WIN32,再判断APPLE,因为macOS也属于UNIX。

我曾经踩过的坑:有一次我在Linux上写 if(UNIX),结果macOS也进来了。后来才发现,macOS的 UNIX 变量也是TRUE。所以判断macOS一定要用 APPLE,并且放在 UNIX 前面。

8.2 while循环:重复直到条件不满足

while循环在CMake里用得不多,但偶尔能派上用场。它的语法很简单:

set(COUNTER 0)
while(COUNTER LESS 5)
  message("当前计数: ${COUNTER}")
  math(EXPR COUNTER "${COUNTER} + 1")
endwhile()

这段代码会输出0到4。注意,CMake没有 ++ 运算符,你得用 math(EXPR ...) 来做算术运算。

说实话,我很少在CMake里用while。因为CMake本身是声明式的,循环多了反而让代码难以理解。但有一种情况我会用——生成重复的配置文件

set(I 0)
while(I LESS 10)
  configure_file(
    "template_${I}.in"
    "output_${I}.txt"
  )
  math(EXPR I "${I} + 1")
endwhile()

嗯,这种场景下while比foreach更直观,因为索引本身就是我们想要的。

8.3 foreach循环:遍历列表的利器

foreach是CMake里最常用的循环。它的用法有好几种,我一个个说。

8.3.1 遍历列表

set(SOURCES main.cpp utils.cpp network.cpp)
foreach(FILE ${SOURCES})
  message("处理源文件: ${FILE}")
endforeach()

这是最基本的用法。注意,${SOURCES} 展开后是三个独立的字符串,foreach会依次赋值给FILE。

8.3.2 遍历范围

foreach(I RANGE 1 5)
  message("索引: ${I}")
endforeach()
# 输出: 1 2 3 4 5

RANGE后面可以跟三个参数:起始值、步长、结束值。比如 RANGE 0 2 10 会输出0, 2, 4, 6, 8, 10。

8.3.3 遍历多个列表(ZIP模式)

这个功能比较新,CMake 3.17才支持。我个人觉得非常实用:

set(NAMES Alice Bob Charlie)
set(AGES 25 30 35)
foreach(NAME AGE IN ZIP_LISTS NAMES AGES)
  message("${NAME} 今年 ${AGE} 岁")
endforeach()
# 输出:
# Alice 今年 25 岁
# Bob 今年 30 岁
# Charlie 今年 35 岁

两个列表长度不一致时,CMake会以较短的为准,不会报错。

实战场景:我在一个项目中需要为多个目标设置不同的编译选项。用ZIP模式,一行代码搞定:
foreach(TARGET FLAG IN ZIP_LISTS TARGETS FLAGS)
  target_compile_options(${TARGET} PRIVATE ${FLAG})
endforeach()

8.4 break和continue:控制循环的节奏

break和continue在CMake里的用法,和C语言完全一样。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

注意,break和continue只能用在循环内部(while或foreach),不能用在if里。如果你在if外面用break,CMake会报错。

我曾经犯过的错:有一次我在foreach里嵌套了一个if,然后在if里面写了break。结果CMake告诉我「break不在循环中」。后来才发现,我少写了一个 endif(),导致break跑到了循环外面。嗯,这种低级错误,排查起来真让人抓狂。

8.5 知识体系总览

下面这张图,帮你把本章的知识点串起来:

CMake流程控制知识体系 流程控制 if / elseif / else / endif 常量/变量判断 字符串/数值比较 文件/逻辑运算 while / foreach while(条件) foreach(列表/范围) foreach(ZIP模式) break / continue break: 跳出整个循环 continue: 跳过当前迭代

这张图把本章的核心内容分成了三大块:条件判断、循环、控制语句。你可以把它当作一个速查表,写CMake时忘了语法,看一眼就明白了。

8.6 避坑指南

最后,我把自己这些年踩过的坑总结一下:

  • 括号不能省:if后面必须跟括号,哪怕条件只有一个变量。
  • 变量展开要小心:在if条件里,变量最好用 ${} 展开,除非你明确知道自己在做什么。
  • 字符串比较用引号if("${VAR}" STREQUAL "value")if(${VAR} STREQUAL value) 更安全。
  • break/continue的位置:确保它们写在循环体内部,别因为少写了一个 endif() 而跑偏。
  • foreach的ZIP模式:需要CMake 3.17+,老版本不支持。

流程控制这块,说白了就是「条件+循环」的组合。你只要记住if怎么用、foreach怎么遍历,大部分场景都能搞定。while用得少,但知道它存在就行。

嗯,这一节就到这里。代码写多了,自然就熟练了。