5、数学运算与逻辑控制:math()命令、if/elseif/else条件判断、逻辑运算符、比较运算符
CMake 虽然是个构建系统,但它的脚本语言能力其实挺强的。说白了,你完全可以把它当成一门轻量级的编程语言来用。数学运算、条件判断、逻辑组合,这些基本功在写复杂 CMakeLists.txt 时几乎天天都要用到。
我个人习惯把这一章叫做「CMake 的大脑」。没有这些控制结构,你的构建脚本就是一潭死水,只能做最简单的编译。有了它们,你才能写出智能的、自适应的构建逻辑。
5.1 math() 命令:CMake 里的计算器
先说说数学运算。CMake 提供了 math() 命令,语法很简单:
math(EXPR <输出变量> "<数学表达式>")
注意,表达式必须用双引号包起来。支持的运算符包括:+、-、*、/、%(取模)、|(按位或)、&(按位与)、^(按位异或)、~(按位取反)、<<(左移)、>>(右移)。
看个例子:
set(A 10)
set(B 3)
math(EXPR SUM "${A} + ${B}")
math(EXPR DIFF "${A} - ${B}")
math(EXPR PROD "${A} * ${B}")
math(EXPR QUOT "${A} / ${B}")
math(EXPR REM "${A} % ${B}")
message("SUM = ${SUM}") # 13
message("DIFF = ${DIFF}") # 7
message("PROD = ${PROD}") # 30
message("QUOT = ${QUOT}") # 3(整数除法,没有小数)
message("REM = ${REM}") # 1
提示:CMake 的除法是整数除法,不会产生小数。如果你需要浮点数运算,得自己想办法,比如用字符串拼接或者调用外部工具。我在项目中遇到过需要计算百分比的情况,最后是用 math() 先乘后除来规避小数问题的。
位运算用得少,但偶尔能救命。比如你想检查一个标志位是否被设置:
set(FLAGS 0x0F)
math(EXPR CHECK "${FLAGS} & 0x02")
if(CHECK)
message("第二位被设置了")
endif()
5.2 if/elseif/else:构建脚本的分岔路口
条件判断是 CMake 里最常用的控制结构。基本语法:
if(<条件>)
# 条件为真时执行
elseif(<条件2>)
# 条件2为真时执行
else()
# 以上都不满足时执行
endif()
注意,endif() 后面可以加注释,比如 endif(),但括号里不能有空格。
举个实际例子。我在做跨平台项目时,经常需要根据操作系统选择不同的源文件:
if(WIN32)
set(PLATFORM_SRC platform_win.cpp)
add_definitions(-DPLATFORM_WIN)
elseif(APPLE)
set(PLATFORM_SRC platform_mac.cpp)
add_definitions(-DPLATFORM_MAC)
elseif(UNIX)
set(PLATFORM_SRC platform_linux.cpp)
add_definitions(-DPLATFORM_LINUX)
else()
message(FATAL_ERROR "不支持的平台")
endif()
注意:elseif 是连写的,中间没有空格。写成 else if 会报错。我曾经因为这个低级错误排查了半小时,后来才发现是语法问题。
5.3 比较运算符:数字 vs 字符串
CMake 的比较运算符分两类:数字比较和字符串比较。这个区分很重要,搞混了会出 bug。
| 类别 | 运算符 | 含义 | 示例 |
|---|---|---|---|
| 数字比较 | EQUAL | 等于 | if(${A} EQUAL 5) |
| NOT_EQUAL | 不等于 | if(${A} NOT_EQUAL 5) |
|
| LESS | 小于 | if(${A} LESS 5) |
|
| GREATER | 大于 | if(${A} GREATER 5) |
|
| LESS_EQUAL | 小于等于 | if(${A} LESS_EQUAL 5) |
|
| GREATER_EQUAL | 大于等于 | if(${A} GREATER_EQUAL 5) |
|
| 字符串比较 | STREQUAL | 字符串相等 | if("${STR}" STREQUAL "hello") |
| STRNOT_EQUAL | 字符串不等 | if("${STR}" STRNOT_EQUAL "hello") |
|
| STRLESS | 字符串小于(字典序) | if("${STR}" STRLESS "hello") |
|
| STRGREATER | 字符串大于(字典序) | if("${STR}" STRGREATER "hello") |
你想想看,为什么要有两套?因为 CMake 里变量都是字符串,但 EQUAL 会尝试把两边转成数字再比较。如果转不了,就会报错。所以比较数字用 EQUAL,比较字符串用 STREQUAL,这是铁律。
5.4 逻辑运算符:AND、OR、NOT
单个条件往往不够用,你需要组合它们。CMake 提供了三个逻辑运算符:
AND:所有条件为真才为真OR:任一条件为真即为真NOT:取反
语法示例:
if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
message("你在 Linux 上使用 GCC 编译器")
endif()
if(NOT MSVC)
message("你不是在 Visual Studio 环境下")
endif()
if(CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
add_definitions(-DENABLE_DEBUG_OUTPUT)
endif()
核心要点:逻辑运算符可以嵌套使用,但要注意括号。CMake 的 if() 条件里,括号用于分组,比如 if((A AND B) OR (C AND D))。不过我个人建议,条件太复杂时,不如拆成多个 if 嵌套,可读性更好。
5.5 条件判断中的「真」与「假」
CMake 对「真」和「假」的定义有点特殊。你可能会踩坑,我当年就吃过这个亏。
以下值被视为 假:
OFF、NO、FALSE、N、0、""(空字符串)、NOTFOUND、以及任何以-NOTFOUND结尾的字符串
以下值被视为 真:
ON、YES、TRUE、Y、1、以及非零数字
注意,"FALSE" 是假,但 " FALSE"(前面有空格)是真。因为 CMake 会先去除变量值两端的空格,但字符串字面量不会。嗯,这里要注意,最好养成习惯:比较字符串时用 STREQUAL,不要依赖隐式真假转换。
5.6 知识体系总览
下面这张图总结了本章的核心逻辑:
5.7 实战:一个完整的条件编译示例
把上面这些知识串起来,写一个实际场景。假设我们要根据 CPU 架构和编译模式,决定是否启用某些优化:
# 获取 CPU 核心数,用于并行编译
include(ProcessorCount)
ProcessorCount(NCPU)
# 根据架构设置不同的优化级别
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
set(ARCH_OPT "-march=x86-64-v3")
math(EXPR PARALLEL_JOBS "${NCPU} - 1")
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|ARM64")
set(ARCH_OPT "-march=armv8-a+simd")
math(EXPR PARALLEL_JOBS "${NCPU} / 2")
else()
set(ARCH_OPT "")
set(PARALLEL_JOBS 1)
endif()
# 调试模式下不启用优化
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
set(ARCH_OPT "")
message(STATUS "调试模式,跳过架构优化")
endif()
# 检查编译器是否支持所选优化
if(NOT "${ARCH_OPT}" STREQUAL "")
try_compile(OPT_SUPPORTED
"${CMAKE_BINARY_DIR}/check"
SOURCES "${CMAKE_SOURCE_DIR}/cmake/check_opt.cpp"
COMPILE_DEFINITIONS "${ARCH_OPT}"
)
if(NOT OPT_SUPPORTED)
message(WARNING "编译器不支持 ${ARCH_OPT},已回退")
set(ARCH_OPT "")
endif()
endif()
add_compile_options("${ARCH_OPT}")
经验之谈:这个例子我实际用过。当时在 ARM 服务器上编译,math(EXPR PARALLEL_JOBS "${NCPU} / 2") 这行代码避免了内存不足导致的编译崩溃。因为 ARM 服务器虽然核心多,但每个核心的内存带宽有限,并行度过高反而会变慢。
5.8 常见陷阱与避坑指南
- 变量展开问题:在
if()条件里,如果变量名本身包含特殊字符,最好用${}展开。比如if(${MY_VAR} EQUAL 5)和if(MY_VAR EQUAL 5)含义不同——前者展开变量值,后者检查变量名是否等于 5。 - 空字符串判断:判断变量是否为空,用
if(DEFINED MY_VAR)而不是if(NOT "${MY_VAR}" STREQUAL "")。后者在变量未定义时会报错。 - 数学运算溢出:CMake 的
math()使用 64 位整数,一般够用。但如果你做位运算,注意符号位的问题。我遇到过左移 63 位后结果变成负数的情况。 - 字符串比较大小写:
STREQUAL是大小写敏感的。需要忽略大小写时,可以用string(TOLOWER ...)先转换再比较。
好了,这一章的内容就这些。数学运算、条件判断、比较和逻辑运算符,这三样东西组合起来,能写出非常灵活的构建逻辑。你写 CMakeLists.txt 的时候,多想想「如果……那么……」这种思维模式,慢慢就会习惯用这些控制结构了。
公众号:蓝海资料掘金营,微信deep3321