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 对「真」和「假」的定义有点特殊。你可能会踩坑,我当年就吃过这个亏。

以下值被视为

  • OFFNOFALSEN0""(空字符串)、NOTFOUND、以及任何以 -NOTFOUND 结尾的字符串

以下值被视为

  • ONYESTRUEY1、以及非零数字

注意,"FALSE" 是假,但 " FALSE"(前面有空格)是真。因为 CMake 会先去除变量值两端的空格,但字符串字面量不会。嗯,这里要注意,最好养成习惯:比较字符串时用 STREQUAL,不要依赖隐式真假转换。

5.6 知识体系总览

下面这张图总结了本章的核心逻辑:

CMake 数学运算与逻辑控制知识体系 数学运算 math() 条件判断 if/elseif/else 逻辑与比较运算符 EXPR 表达式 + - * / % 位运算 if() elseif() else() endif() 真假值判定规则 数字比较:EQUAL LESS GREATER 字符串比较:STREQUAL STRLESS 逻辑:AND OR NOT 三者结合:用 math() 计算结果,用比较运算符判断,用 if 控制流程

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