C/C++开发中CMake语法深度解析之控制流、列表与函数实战指南并不只看表面做法关键还要理解相关条件、限制和后续影响。
在 C/C++ 跨平台开发中CMake 早已成为事实标准。但很多开发者仅停留在"复制粘贴模板代码"的阶段当需要处理复杂的条件判断、批量文件操作或代码复用场景时往往束手无策。控制流语句、列表操作和函数是 CMake 脚本的三大核心支柱掌握它们才能真正从"模板搬运工"转变为"构建系统设计师"。下文会结合 CMake 源码逻辑和实战代码从原理到实践逐行拆解 if 判断、foreach/while 循环、list 函数和自定义函数的所有用法帮你彻底攻克 CMake 脚本编写的核心难点。
if() 是 CMake 中最基础也最容易出错的控制流语句。它的行为与大多数编程语言有显著差异特别是在变量解析和布尔值判断方面很多坑都源于对其底层逻辑的不了解。
CMake 的 if 语句支持完整的条件分支结构
if(<condition>) <commands>elseif(<condition>) # 可选可重复 <commands>else() # 可选 <commands>endif()
核心特性
() 可用于改变运算优先级CMake 中布尔值的判断规则非常特殊我们可以从 CMake 源码中的 GetBooleanValue 函数一探究竟
// CMake 源码片段GetBooleanValue 函数逻辑bool GetBooleanValue(const std::string& arg) const { // 第一步检查命名常量 if (cmIsOn(arg)) return true; // "1", "ON", "YES", "TRUE", "Y" if (cmIsOff(arg)) return false; // "0", "OFF", "NO", "FALSE", "N", "IGNORE", "NOTFOUND" // 第二步检查是否为数字 char* end; double d = std::strtod(arg.c_str(), &end); if (*end == '') { // 整个字符串都是数字 return static_cast<bool>(d); } // 第三步检查是否为已定义的变量 cmValue def = this->GetDefinitionIfUnquoted(arg); return !def.IsOff();}基于这个逻辑我们可以总结出完整的布尔值判断规则
| 输入类型 | 判断为 true 的情况 | 判断为 false 的情况 |
|---|---|---|
| 命名常量 | “1”, “ON”, “YES”, “TRUE”, “Y”大小写不敏感 | “0”, “OFF”, “NO”, “FALSE”, “N”, “IGNORE”, “NOTFOUND”以及以 “-NOTFOUND” 结尾的字符串 |
| 数字 | 非零数字包括浮点数 | 0 |
| 变量 | 变量已定义且值不是 false 常量 | 变量未定义或值为 false 常量 |
| 字符串字面量 | 仅当字符串内容是 true 常量 | 其他所有字符串 |
实战验证代码
cmake_minimum_required(VERSION 3.18)project(IfBooleanDemo)# 1. 命名常量测试message("=== 命名常量测试 ===")if("ON") message("1: ON = true") else() message("1: ON = false") endif()if("OFF") message("2: OFF = true") else() message("2: OFF = false") endif()if("YES") message("3: YES = true") else() message("3: YES = false") endif()if("NO") message("4: NO = true") else() message("4: NO = false") endif()# 2. 数字测试message("n=== 数字测试 ===")if("1.1") message("5: 1.1 = true") else() message("5: 1.1 = false") endif()if("0") message("6: 0 = true") else() message("6: 0 = false") endif()if("123A") message("7: 123A = true") else() message("7: 123A = false") endif()# 3. 变量测试message("n=== 变量测试 ===")set(OS "linux")if(OS) message("8: OS(linux) = true") else() message("8: OS(linux) = false") endif()set(OS "OFF")if(OS) message("9: OS(OFF) = true") else() message("9: OS(OFF) = false") endif()# 4. 字符串字面量测试message("n=== 字符串字面量测试 ===")if("abc") message("10: "abc" = true") else() message("10: "abc" = false") endif()if("on") message("11: "on" = true") else() message("11: "on" = false") endif()运行结果
=== 命名常量测试 ===
1: ON = true
2: OFF = false
3: YES = true
4: NO = false
=== 数字测试 ===
5: 1.1 = true
6: 0 = false
7: 123A = false
=== 变量测试 ===
8: OS(linux) = true
9: OS(OFF) = false
=== 字符串字面量测试 ===
10: "abc" = false
11: "on" = true
关键陷阱
${} 包裹if(OS) 等价于 if(${OS})if(ENV{VAR}) 测试必须先展开if($ENV{VAR})CMake 提供了丰富的比较操作符适用于不同类型的数据
EQUAL等于LESS小于GREATER大于LESS_EQUAL小于等于GREATER_EQUAL大于等于if("20" LESS "30") message("20 < 30 = true")endif()STREQUAL等于STRLESS字典序小于STRGREATER字典序大于if("abc" STRLESS "abd") message("abc < abd (字典序) = true")endif()VERSION_EQUAL版本等于VERSION_LESS版本小于VERSION_GREATER版本大于set(VERSION "1.2.3")if(VERSION VERSION_LESS "1.3.0") message("版本 1.2.3 < 1.3.0 = true")endif()PATH_EQUAL路径相等CMake 3.24+if("/a/b/c" PATH_EQUAL "/a//b/c/") message("路径相等 = true") # 多个分隔符会被合并endif()NOT逻辑非AND逻辑与OR逻辑或if(NOT "OFF") message("NOT OFF = true")endif()if("ON" AND "OFF") message("ON AND OFF = true")else() message("ON AND OFF = false")endif()if("ON" OR "OFF") message("ON OR OFF = true")endif()CMake 提供了多种检测命令用于检查文件、目标、变量等是否存在
| 检测命令 | 功能 |
|---|---|
EXISTS <path> | 检查文件或目录是否存在 |
IS_DIRECTORY <path> | 检查是否为目录 |
IS_ABSOLUTE <path> | 检查是否为绝对路径 |
COMMAND <command> | 检查命令是否存在 |
TARGET <target> | 检查目标是否存在 |
DEFINED <var> | 检查变量是否定义 |
DEFINED CACHE{<var>} | 检查缓存变量是否定义 |
DEFINED ENV{<var>} | 检查环境变量是否定义 |
实战示例
# 检查文件是否存在if(EXISTS ${CMAKE_CURRENT_LIST_FILE}) message("当前 CMakeLists.txt 存在")endif()# 检查目标是否存在add_library(MyMath INTERFACE IMPORTED)if(TARGET MyMath) message("目标 MyMath 存在")endif()# 检查变量是否定义set(NAME "bit")if(DEFINED NAME) message("变量 NAME 已定义")endif()# 检查环境变量是否定义if(DEFINED ENV{PATH}) message("环境变量 PATH 已定义")endif()CMake 提供了两种循环结构foreach 和 while。其中 foreach 用于遍历列表或范围是最常用的循环方式while 用于在条件为真时重复执行代码块。
foreach 循环语法非常灵活共有五种常用形式
foreach(<variable> <item1> <item2> ...) # 循环体endforeach()
示例
foreach(X "1" "2;3;4" "abc") message("X1: ${X}")endforeach()输出
X1: 1X1: 2;3;4X1: abc
注意分号分隔的字符串会被当作一个整体元素。
# 遍历 0 到 stop包含foreach(<variable> RANGE <stop>)# 遍历 start 到 stop包含步长为 stepforeach(<variable> RANGE <start> <stop> [<step>])
示例
# 遍历 0 到 10foreach(X RANGE 10) message("X2: ${X}")endforeach()# 遍历 10 到 20步长为 3foreach(X RANGE 10 20 3) message("X3: ${X}")endforeach()输出
X2: 0
X2: 1
...
X2: 10
X3: 10
X3: 13
X3: 16
X3: 19
foreach(<variable> IN ITEMS <item1> <item2> ...)
与基本形式类似但更明确地表示后面是字面量元素
foreach(X IN ITEMS "1" "2;3;4" "abc") message("X4: ${X}")endforeach()输出与基本形式完全相同。
foreach(<variable> IN LISTS <list_variable1> <list_variable2> ...)
这是最常用的形式用于遍历存储在变量中的列表
set(L1 "1")set(L2 "2;3;4")set(L3 "abc")foreach(X IN LISTS L1 L2 L3) message("X5: ${X}")endforeach()输出
X5: 1
X5: 2
X5: 3
X5: 4
X5: abc
关键区别IN LISTS 会自动展开变量中的列表按分号拆分而 IN ITEMS 不会。
while 循环在条件为真时重复执行代码块
while(<condition>) # 循环体endwhile()
示例计数器循环
set(COUNT 1)while(COUNT LESS_EQUAL 5) message(STATUS "计数: ${COUNT}") math(EXPR COUNT "${COUNT} + 1") # 递增计数endwhile()输出
-- 计数: 1
-- 计数: 2
-- 计数: 3
-- 计数: 4
-- 计数: 5
注意事项
math(EXPR) 进行数值计算在 CMake 中列表本质上是用分号分隔的字符串。list 函数提供了丰富的操作方法用于处理这种特殊的数据结构。
list(COMMAND <list_variable> [args...])
list 函数的第一个参数是操作命令第二个参数是要操作的列表变量。
APPEND在列表末尾添加元素PREPEND在列表开头添加元素INSERT在指定位置插入元素set(SRCS "main.cpp" "utils.cpp" "math.cpp")# 末尾添加list(APPEND SRCS "network.cpp")message("APPEND: ${SRCS}") # main.cpp;utils.cpp;math.cpp;network.cpp# 开头添加list(PREPEND SRCS "config.cpp")message("PREPEND: ${SRCS}") # config.cpp;main.cpp;utils.cpp;math.cpp;network.cpp# 指定位置插入索引从 0 开始list(INSERT SRCS 2 "logger.cpp")message("INSERT: ${SRCS}") # config.cpp;main.cpp;logger.cpp;utils.cpp;math.cpp;network.cppREMOVE_ITEM移除指定值的元素REMOVE_AT移除指定索引的元素REMOVE_DUPLICATES移除重复元素# 移除指定值list(REMOVE_ITEM SRCS "math.cpp")message("REMOVE_ITEM: ${SRCS}")# 移除指定索引list(REMOVE_AT SRCS 1)message("REMOVE_AT: ${SRCS}")# 移除重复list(APPEND SRCS "utils.cpp")list(REMOVE_DUPLICATES SRCS)message("REMOVE_DUPLICATES: ${SRCS}")LENGTH获取列表长度GET获取指定索引的元素SORT排序列表REVERSE反转列表JOIN用指定字符串连接列表元素# 获取长度list(LENGTH SRCS LEN)message("列表长度: ${LEN}")# 获取元素list(GET SRCS 0 FIRST_ITEM)message("第一个元素: ${FIRST_ITEM}")# 排序list(SORT SRCS)message("排序后: ${SRCS}")# 反转list(REVERSE SRCS)message("反转后: ${SRCS}")# 连接list(JOIN SRCS " | " JOINED)message("连接后: ${JOINED}")函数是组织 CMake 代码、提高复用性的重要工具。通过定义函数可以封装复杂的操作避免代码重复使构建脚本更加清晰易读。
# 定义函数function(<function_name> <arg1> <arg2> ...) # 函数体endfunction()# 调用函数<function_name>(<value1> <value2> ...)
示例简单的加法函数
function(add_numbers A B) math(EXPR RESULT "${A} + ${B}") message("${A} + ${B} = ${RESULT}")endfunction()# 调用add_numbers(3 5) # 输出: 3 + 5 = 8CMake 函数提供了一组内置变量用于访问传递给函数的参数
| 内置变量 | 描述 | 示例调用 func(a b c d e) |
|---|---|---|
${ARGC} | 参数总数 | 5 |
${ARGV} | 所有参数组成的列表 | “a;b;c;d;e” |
${ARGV0} | 第一个参数 | “a” |
${ARGV1} | 第二个参数 | “b” |
${ARGN} | 剩余参数可变参数 | 若函数定义为 func(a b)则为 “c;d;e” |
实战示例打印所有参数
function(print_args first second) message("ARGC = ${ARGC}") message("ARGV = ${ARGV}") message("ARGV0 = ${ARGV0}") message("ARGV1 = ${ARGV1}") message("ARGV2 = ${ARGV2}") message("ARGN = ${ARGN}")endfunction()# 调用print_args(A B C D E)输出
ARGC = 5
ARGV = A;B;C;D;E
ARGV0 = A
ARGV1 = B
ARGV2 = C
ARGN = C;D;E
CMake 函数具有独立的作用域函数内部定义的变量默认是局部变量不会影响外部作用域。
set(SCOP "全局作用域")function(print_var) # 可以读取父作用域的变量 message("函数内读取: ${SCOP}") # 函数内部修改变量默认只影响局部作用域 set(SCOP "函数作用域") message("函数内修改后: ${SCOP}")endfunction()print_var()# 外部变量的值不变message("函数外部: ${SCOP}")输出
函数内读取: 全局作用域
函数内修改后: 函数作用域
函数外部: 全局作用域
如果需要将函数内部的变量值传递到父作用域可以使用 PARENT_SCOPE 选项
function(print_var) message("函数内读取: ${SCOP}") # 使用 PARENT_SCOPE 修改父作用域的变量 set(SCOP "函数作用域" PARENT_SCOPE) # 注意当前函数作用域的变量值仍然不变 message("函数内修改后: ${SCOP}")endfunction()print_var()message("函数外部: ${SCOP}")输出
函数内读取: 全局作用域
函数内修改后: 全局作用域 # 关键当前作用域的值没有变
函数外部: 函数作用域 # 父作用域的值被修改了
底层原理
PARENT_SCOPE 直接修改父作用域的原始内存空间CMake 函数没有传统的返回值语法但可以通过以下两种方式"返回"结果
方式 1通过 PARENT_SCOPE 设置变量推荐如果你理解这个的话
function(sum A B OUTPUT_VAR) math(EXPR RESULT "${A} + ${B}") set(${OUTPUT_VAR} ${RESULT} PARENT_SCOPE)endfunction()# 调用sum(3 5 RESULT)message("3 + 5 = ${RESULT}") # 输出: 3 + 5 = 8方式 2通过缓存变量全局可见
function(sum_global A B) math(EXPR RESULT "${A} + ${B}") set(GLOBAL_SUM ${RESULT} CACHE INTERNAL "全局求和结果" FORCE)endfunction()# 调用sum_global(3 5)message("全局结果: ${GLOBAL_SUM}") # 输出: 全局结果: 8注意缓存变量会跨多次 CMake 运行保持不变使用时需谨慎。
总结
1. if 判断
if($ENV{VAR}) 测试不能直接用 if(ENV{VAR})2. 循环
IN LISTS 会展开变量中的列表IN ITEMS 不会3. list 函数
4. 函数
ARGC、ARGV、ARGN 等PARENT_SCOPE 修改父作用域PARENT_SCOPE 的陷阱当前函数作用域的值不变PARENT_SCOPE推荐和缓存变量