6、CMake条件与循环:if/elseif/else、foreach、while、函数与宏(function/macro)
说实话,很多C++开发者写CMakeLists.txt时,就是一路平铺直叙地写下去。项目小的时候还行,一旦项目复杂起来——比如要支持多平台、多编译器、可选功能模块——你就会发现,没有条件判断和循环,根本没法写。
我个人习惯把CMake里的条件、循环、函数和宏,看作是「CMake的编程能力核心」。掌握了它们,你写的就不是配置文件了,而是一个真正的构建脚本。
6.1 条件判断:if/elseif/else
条件判断是CMake里最常用的控制结构。我见过不少新手在if里写错变量引用方式,导致条件永远不成立。咱们先看基本语法:
if(condition)
# 条件为真时执行
elseif(another_condition)
# 另一个条件
else()
# 都不满足
endif()
这里有个关键点:CMake的if对变量名和值有一套自己的解析规则。比如:
set(MY_VAR "ON")
if(MY_VAR)
message("MY_VAR为真")
endif()
# 等价于
if(${MY_VAR})
message("MY_VAR为真")
endif()
嗯,这里要注意:if(MY_VAR) 这种写法,CMake会先尝试把 MY_VAR 当作变量名去取值,如果取到了非空且非假的值,就视为真。但如果你的变量名和某个关键字冲突了呢?比如:
set(TRUE "FALSE") # 别这么干!
if(TRUE)
# 这里依然为真,因为TRUE是内置常量
endif()
MY_ANDROID。
常用的条件表达式:
| 表达式 | 说明 | 示例 |
|---|---|---|
if(DEFINED var) |
变量是否已定义 | 检查变量是否存在 |
if(NOT condition) |
逻辑非 | if(NOT WIN32) |
if(var1 STREQUAL var2) |
字符串相等 | 比较两个字符串 |
if(var1 GREATER var2) |
数值大于 | 版本号比较 |
if(EXISTS path) |
文件或目录是否存在 | 检查头文件 |
if(TARGET name) |
目标是否存在 | 检查库是否已定义 |
${} 显式引用出来,虽然CMake会自动解析,但这样代码可读性更好,别人一看就知道这是个变量。
6.2 循环:foreach 和 while
循环在CMake里主要用来处理列表。你想想看,如果你有几十个源文件要添加到同一个目标里,一个一个写 target_sources() 不得累死?
foreach 循环
foreach 有几种用法,我按常用程度列出来:
# 用法1:遍历列表
set(SOURCES main.cpp utils.cpp network.cpp)
foreach(src ${SOURCES})
message("处理源文件: ${src}")
endforeach()
# 用法2:范围遍历
foreach(i RANGE 1 10)
message("迭代: ${i}")
endforeach()
# 用法3:同时遍历多个列表(CMake 3.17+)
foreach(src IN ZIP_LISTS HEADERS SOURCES)
message("${src_0} -> ${src_1}")
endforeach()
我在项目中遇到过最典型的场景,就是根据不同的编译选项,动态生成源文件列表:
set(BASE_SRCS core.cpp math.cpp)
if(ENABLE_NETWORK)
list(APPEND BASE_SRCS tcp_client.cpp http_parser.cpp)
endif()
if(ENABLE_GUI)
list(APPEND BASE_SRCS window.cpp widget.cpp)
endif()
# 然后统一添加
foreach(src ${BASE_SRCS})
target_sources(myapp PRIVATE src/${src})
endforeach()
while 循环
while 用得少一些,但某些场景下很顺手:
set(counter 0)
while(counter LESS 5)
message("计数: ${counter}")
math(EXPR counter "${counter} + 1")
endwhile()
break 和 continue 语句(至少3.20之前没有)。如果你需要提前退出循环,得用 if 包一下逻辑。我曾经因为这个特性,写了一个死循环,CMake跑了5分钟还没结束……后来加了超时检测。
6.3 函数与宏:function/macro
函数和宏是CMake里实现代码复用的核心手段。说白了,就是把你重复写的那些 add_library、target_include_directories 之类的逻辑,封装成一个可调用的单元。
函数(function)
函数会创建一个新的作用域。这意味着函数内部定义的变量,默认不会影响外部:
function(add_my_library lib_name)
add_library(${lib_name} STATIC ${lib_name}.cpp)
target_include_directories(${lib_name} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
message("库 ${lib_name} 已创建")
endfunction()
# 调用
add_my_library(utils)
add_my_library(network)
如果你想从函数里往外传值,可以用 set(PARENT_SCOPE):
function(get_output_dir out_var)
set(${out_var} "${CMAKE_BINARY_DIR}/output" PARENT_SCOPE)
endfunction()
get_output_dir(MY_OUTPUT)
message("输出目录: ${MY_OUTPUT}")
宏(macro)
宏和函数很像,但宏不创建新作用域。宏里的变量直接操作调用者的作用域:
macro(print_var var_name)
message("${var_name} = ${${var_name}}")
endmacro()
set(MY_VAR "hello")
print_var(MY_VAR) # 输出: MY_VAR = hello
嗯,这里要注意:宏里如果用了 set(),它会直接修改调用者的变量。这既是优点也是缺点——用得好可以省去 PARENT_SCOPE 的麻烦,用不好就会莫名其妙地污染变量。
| 特性 | function | macro |
|---|---|---|
| 作用域 | 独立作用域 | 调用者作用域 |
| 变量修改 | 需 PARENT_SCOPE | 直接修改 |
| 参数传递 | 值传递 | 文本替换 |
| 推荐场景 | 封装复杂逻辑 | 简单代码替换 |
function。宏的文本替换机制有时候会带来意想不到的副作用。我在一个大型项目里见过因为宏嵌套调用,导致变量名被意外展开成奇怪字符串的bug,查了两天才定位到。
6.4 知识体系总览
下面这张图把本章的核心知识点串起来了,你可以对照着回顾一下:
6.5 实战经验总结
最后,分享几个我在实际项目中积累的经验:
- 条件判断里多用括号:复杂的条件用
()分组,可读性会好很多。比如if((A AND B) OR (C AND NOT D))。 - foreach 里慎用 list 修改:在 foreach 循环里用
list(REMOVE_ITEM)或list(APPEND)修改正在遍历的列表,行为是未定义的。我踩过这个坑。 - 函数参数用命名参数:CMake 3.18+ 支持
cmake_parse_arguments,建议用这个来解析命名参数,比靠位置传参清晰得多。 - 宏里不要用 return():宏里用
return()会直接跳出调用者的上下文,而不是宏本身。这个特性让很多人吃过亏。
公众号:蓝海资料掘金营,微信deep3321