4、CMake条件与循环:if/elseif/else 逻辑判断、foreach() 循环遍历列表、while() 循环、函数与宏(function/macro)

说实话,CMake 这门「语言」刚上手时,很多人觉得它就是个配置工具。但真正把项目做复杂了,你会发现——没有条件判断和循环,你连个像样的跨平台构建都写不出来。今天我们就来聊聊 CMake 里的控制流,以及我这些年踩过的坑。

4.1 条件判断:if/elseif/else

条件判断是 CMake 里最基础的控制结构。说白了,就是根据不同的平台、编译器、选项,走不同的构建路径。

4.1.1 基本语法

if(condition)
    # 条件为真时执行
elseif(another_condition)
    # 另一个条件
else()
    # 都不满足时
endif()

嗯,这里要注意:endif() 后面可以加注释,比如 endif(),但我不建议你写太花哨的东西,保持干净就好。

4.1.2 常用条件表达式

表达式 含义 示例
WIN32 Windows 平台 if(WIN32)
APPLE macOS / iOS if(APPLE)
UNIX 类 Unix 系统 if(UNIX)
MSVC Visual Studio 编译器 if(MSVC)
CMAKE_CXX_COMPILER_ID STREQUAL "Clang" 编译器 ID 比较 if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
DEFINED VAR 变量是否已定义 if(DEFINED MY_VAR)
EXISTS file 文件或目录是否存在 if(EXISTS "${CMAKE_SOURCE_DIR}/config.h")
我的习惯: 判断平台时,我一般先写 if(WIN32),再写 elseif(APPLE),最后 else() 留给 Linux。这样逻辑清晰,不容易漏掉。

4.1.3 逻辑运算符

if(condition1 AND condition2)
if(condition1 OR condition2)
if(NOT condition)

举个例子,我在项目中遇到过需要同时判断平台和编译器的情况:

if(WIN32 AND MSVC)
    message(STATUS "Windows + MSVC,开启特定优化")
    add_compile_options(/O2 /GL)
elseif(WIN32 AND MINGW)
    message(STATUS "Windows + MinGW,使用 GCC 风格")
    add_compile_options(-O2)
endif()
避坑指南: 我曾经在 if() 里直接写 if(${VAR}),结果 VAR 是空字符串时,CMake 会报错。正确做法是 if(VAR) 或者 if(DEFINED VAR)。记住:CMake 的 if 里,变量名不用加 ${}。

4.2 foreach() 循环遍历列表

foreach 是我用得最多的循环。你想想看,CMake 里到处都是列表——源文件列表、库列表、子目录列表……没有 foreach,你只能一个个手写,那得多累。

4.2.1 基本用法

set(SOURCES
    src/main.cpp
    src/utils.cpp
    src/network.cpp
)

foreach(SRC ${SOURCES})
    message(STATUS "处理源文件: ${SRC}")
    # 可以在这里做更多操作,比如设置编译选项
endforeach()

4.2.2 带索引的遍历

有时候你需要知道当前是第几个元素。CMake 3.17 之后支持 foreach() 带 RANGE 和 ZIP_LISTS,但我个人习惯用老办法:

set(INDEX 0)
foreach(SRC ${SOURCES})
    math(EXPR INDEX "${INDEX} + 1")
    message(STATUS "第 ${INDEX} 个源文件: ${SRC}")
endforeach()

4.2.3 遍历多个列表

我记得有一次需要同时遍历源文件和对应的头文件目录,我是这么写的:

set(SOURCES src/a.cpp src/b.cpp)
set(HEADERS include/a.h include/b.h)

list(LENGTH SOURCES LEN)
math(EXPR MAX_IDX "${LEN} - 1")

foreach(IDX RANGE ${MAX_IDX})
    list(GET SOURCES ${IDX} SRC)
    list(GET HEADERS ${IDX} HDR)
    message(STATUS "配对: ${SRC} <-> ${HDR}")
endforeach()
核心要点: foreach 的 RANGE 模式支持 RANGE start stop [step],比如 foreach(i RANGE 1 10 2) 会得到 1,3,5,7,9。

4.3 while() 循环

while 循环在 CMake 里用得相对少一些,但某些场景下它比 foreach 更灵活。比如你不知道循环次数,需要根据条件动态退出。

4.3.1 基本语法

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

4.3.2 实际应用场景

我在项目中遇到过需要不断查找依赖库的情况。比如某个库可能安装在多个路径下,我需要逐个尝试:

set(SEARCH_PATHS
    /usr/local/lib
    /opt/lib
    /home/user/local/lib
)

set(INDEX 0)
list(LENGTH SEARCH_PATHS TOTAL)
set(FOUND_LIB FALSE)

while(INDEX LESS TOTAL AND NOT FOUND_LIB)
    list(GET SEARCH_PATHS ${INDEX} PATH)
    if(EXISTS "${PATH}/libfoo.so")
        message(STATUS "找到 libfoo.so 在: ${PATH}")
        set(FOUND_LIB TRUE)
    endif()
    math(EXPR INDEX "${INDEX} + 1")
endwhile()
注意: while 循环一定要有退出条件,否则会死循环。我曾经调试一个 CMake 脚本,发现构建卡住了,排查了半天才发现 while 条件写错了,变量一直没更新。嗯,从那以后我写 while 都会先检查退出条件。

4.4 函数与宏(function/macro)

函数和宏是 CMake 里复用代码的核心手段。很多人分不清它们的区别,其实很简单:

  • function:创建新的作用域,变量修改不影响外部
  • macro:在调用者作用域内展开,变量修改会影响外部

4.4.1 定义函数

function(add_my_library TARGET_NAME SOURCES)
    add_library(${TARGET_NAME} STATIC ${SOURCES})
    target_include_directories(${TARGET_NAME}
        PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include
    )
    target_compile_options(${TARGET_NAME}
        PRIVATE -Wall -Wextra
    )
    message(STATUS "已创建库: ${TARGET_NAME}")
endfunction()

# 调用
add_my_library(mylib "src/a.cpp;src/b.cpp")

4.4.2 定义宏

macro(print_variable VAR_NAME)
    message(STATUS "${VAR_NAME} = ${${VAR_NAME}}")
endmacro()

set(MY_VAR "hello")
print_variable(MY_VAR)  # 输出: MY_VAR = hello

4.4.3 函数 vs 宏:关键区别

特性 function macro
作用域 独立作用域 调用者作用域
变量修改 不影响外部 影响外部
返回值 通过 set(PARENT_SCOPE) 直接修改外部变量
参数传递 按值传递 按文本替换
推荐场景 大多数情况 简单文本替换
我的建议: 除非你明确需要宏的「文本替换」特性,否则一律用 function。宏的副作用很难调试,我见过太多因为宏修改了外部变量导致构建行为诡异的案例。

4.4.4 函数返回值技巧

函数不能直接返回值,但可以通过 set(... PARENT_SCOPE) 把变量传递到上层:

function(get_platform_suffix RESULT_VAR)
    if(WIN32)
        set(${RESULT_VAR} "_win" PARENT_SCOPE)
    elseif(APPLE)
        set(${RESULT_VAR} "_mac" PARENT_SCOPE)
    else()
        set(${RESULT_VAR} "_linux" PARENT_SCOPE)
    endif()
endfunction()

get_platform_suffix(SUFFIX)
message(STATUS "平台后缀: ${SUFFIX}")

4.5 知识体系图

下面这张图帮你理清 CMake 条件与循环的整体脉络:

CMake 条件与循环知识体系 CMake 控制流 if/elseif/else foreach() 循环 while() 循环 平台判断 (WIN32/APPLE/UNIX) 编译器判断 (MSVC/Clang/GCC) 变量/文件存在性检查 遍历源文件列表 RANGE 模式遍历索引 多列表并行遍历 动态条件循环 依赖库路径搜索 function() 与 macro() 独立作用域,推荐使用 文本替换,谨慎使用

4.6 实战经验总结

最后分享几个我这些年积累的小经验:

  • 条件判断优先用平台变量:WIN32、APPLE、UNIX 这些是 CMake 内置的,比你自己写 if(CMAKE_SYSTEM_NAME STREQUAL "Windows") 更简洁、更可靠。
  • foreach 比 while 更安全:除非你真的需要动态退出条件,否则用 foreach 遍历列表,不容易出 bug。
  • 函数参数用大写:我个人习惯函数参数用大写命名,比如 TARGET_NAME、SOURCES,这样一眼就能看出是参数,不是普通变量。
  • 宏里慎用 return():宏里的 return() 行为跟函数不一样,它不会退出宏,而是退出整个 CMake 处理。我吃过这个亏,后来就很少用宏了。
一个小技巧: 调试 CMake 脚本时,多用 message(STATUS "...") 打印变量值。我经常在 if 分支里加 message(STATUS "进入 WIN32 分支") 这样的日志,方便追踪构建流程。

好了,CMake 的条件与循环就聊到这里。这些基础控制结构,加上之前学的变量和列表操作,已经能应付大部分跨平台构建场景了。记住:写 CMake 跟写代码一样,清晰比炫技更重要。

可可精品资料库