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。
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会以较短的为准,不会报错。
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会报错。
endif(),导致break跑到了循环外面。嗯,这种低级错误,排查起来真让人抓狂。
8.5 知识体系总览
下面这张图,帮你把本章的知识点串起来:
这张图把本章的核心内容分成了三大块:条件判断、循环、控制语句。你可以把它当作一个速查表,写CMake时忘了语法,看一眼就明白了。
8.6 避坑指南
最后,我把自己这些年踩过的坑总结一下:
- 括号不能省:if后面必须跟括号,哪怕条件只有一个变量。
- 变量展开要小心:在if条件里,变量最好用
${}展开,除非你明确知道自己在做什么。 - 字符串比较用引号:
if("${VAR}" STREQUAL "value")比if(${VAR} STREQUAL value)更安全。 - break/continue的位置:确保它们写在循环体内部,别因为少写了一个
endif()而跑偏。 - foreach的ZIP模式:需要CMake 3.17+,老版本不支持。
流程控制这块,说白了就是「条件+循环」的组合。你只要记住if怎么用、foreach怎么遍历,大部分场景都能搞定。while用得少,但知道它存在就行。
嗯,这一节就到这里。代码写多了,自然就熟练了。