8. 函数与宏:function命令、macro命令、参数处理、返回值、函数重载、递归函数
说到CMake的函数和宏,我得先坦白一件事。我刚开始用CMake那会儿,总觉得这玩意儿就是个“高级点的配置文件”,直到项目规模膨胀到几十个模块,我才意识到——没有函数和宏,你根本活不下去。
说白了,函数和宏就是CMake里的“代码复用利器”。它们能帮你把重复的逻辑封装起来,让构建脚本变得清爽、可维护。今天咱们就把这块彻底讲透。
8.1 function 与 macro:看似相似,实则不同
CMake里定义可复用代码块有两种方式:function() 和 macro()。语法上几乎一模一样,但行为上有本质区别。
# 函数定义
function(my_function arg1 arg2)
message("arg1 = ${arg1}, arg2 = ${arg2}")
set(result "hello" PARENT_SCOPE)
endfunction()
# 宏定义
macro(my_macro arg1 arg2)
message("arg1 = ${arg1}, arg2 = ${arg2}")
set(result "hello")
endmacro()
嗯,这里要注意:函数有自己的作用域,宏没有。什么意思呢?
- 函数内部用
set()设置的变量,默认只在函数内部可见。想往外传?必须加PARENT_SCOPE。 - 宏内部用
set()设置的变量,直接就在调用者的作用域里生效了。
我在项目中遇到过好几次这样的坑:用宏封装了一个设置变量的逻辑,结果调用后发现调用者的变量被意外覆盖了。排查了半天,最后发现是宏的“副作用”。从那以后,我个人的习惯是:能用函数就别用宏,除非你真的需要宏那种“直接注入”的效果。
核心区别总结
| 特性 | function | macro |
|---|---|---|
| 作用域 | 独立作用域 | 调用者作用域 |
| 变量传递 | 需 PARENT_SCOPE | 直接可见 |
| 参数处理 | 值传递(副本) | 文本替换(展开) |
| 推荐场景 | 大多数情况 | 需要修改调用者变量 |
8.2 参数处理:别让参数把你绕晕
CMake的参数处理其实挺灵活的。除了普通的命名参数,还有几个特殊变量你得记住:
ARGC:参数个数ARGV:所有参数列表ARGN:超出命名参数的部分ARGV0、ARGV1...:按索引访问
function(print_args first second)
message("first = ${first}")
message("second = ${second}")
message("total args = ${ARGC}")
message("all args = ${ARGV}")
message("extra args = ${ARGN}")
endfunction()
print_args(a b c d e)
# 输出:
# first = a
# second = b
# total args = 5
# all args = a;b;c;d;e
# extra args = c;d;e
你想想看,如果参数数量不确定,ARGN 就派上大用场了。我曾经写过一个批量添加测试用例的函数,就是用 ARGN 接收不定数量的源文件,然后逐个注册测试。
小技巧:处理参数时,建议先用 cmake_parse_arguments() 做解析,尤其是参数里有可选参数、关键字参数的时候。这比手动解析 ARGV 要靠谱得多。
8.3 返回值:函数怎么把结果传出来?
函数没有 return 语句返回值这一说。那怎么把结果传出来?三种方式:
- 设置变量并加 PARENT_SCOPE:最常用
- 设置全局属性:用
set_property(GLOBAL ...) - 通过输出参数:把变量名作为参数传进去,函数内部修改它
# 方式1:PARENT_SCOPE
function(add_one value result_var)
math(EXPR result "${value} + 1")
set(${result_var} ${result} PARENT_SCOPE)
endfunction()
add_one(5 output)
message("output = ${output}") # 输出:output = 6
# 方式2:输出参数
function(get_platform out_var)
if(WIN32)
set(${out_var} "Windows" PARENT_SCOPE)
elseif(APPLE)
set(${out_var} "macOS" PARENT_SCOPE)
else()
set(${out_var} "Linux" PARENT_SCOPE)
endif()
endfunction()
get_platform(platform)
message("platform = ${platform}")
我个人比较喜欢第二种方式——把变量名作为参数传进去。这样调用方一看就知道这个参数是用来接收结果的,语义清晰。
8.4 函数重载?不存在的,但有替代方案
CMake不支持函数重载。你定义两个同名函数,后面的会覆盖前面的。那怎么办?
我常用的做法是:用关键字参数模拟重载。通过 cmake_parse_arguments() 判断传入了哪些参数,走不同的逻辑分支。
function(configure_target target)
set(options STATIC SHARED)
set(oneValueArgs OUTPUT_NAME)
set(multiValueArgs SOURCES DEPENDENCIES)
cmake_parse_arguments(PARSE_ARGV 1 "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if(PARSE_STATIC)
add_library(${target} STATIC)
elseif(PARSE_SHARED)
add_library(${target} SHARED)
else()
add_executable(${target})
endif()
if(PARSE_SOURCES)
target_sources(${target} PRIVATE ${PARSE_SOURCES})
endif()
if(PARSE_DEPENDENCIES)
target_link_libraries(${target} PRIVATE ${PARSE_DEPENDENCIES})
endif()
if(PARSE_OUTPUT_NAME)
set_target_properties(${target} PROPERTIES OUTPUT_NAME ${PARSE_OUTPUT_NAME})
endif()
endfunction()
# 调用时就像重载一样灵活
configure_target(my_lib STATIC SOURCES a.cpp b.cpp)
configure_target(my_app SOURCES main.cpp DEPENDENCIES my_lib)
你看,一个函数处理了静态库、动态库、可执行文件三种情况。这就是CMake里的“伪重载”。
8.5 递归函数:小心别把栈搞炸了
CMake支持递归调用。但说实话,我在实际项目中很少用。为什么?因为CMake的递归效率不高,而且容易把调用栈搞炸。
不过有些场景确实适合递归,比如遍历目录树:
function(collect_sources dir result_var)
file(GLOB children RELATIVE ${dir} ${dir}/*)
set(sources "")
foreach(child ${children})
set(full_path "${dir}/${child}")
if(IS_DIRECTORY ${full_path})
# 递归调用
collect_sources(${full_path} sub_sources)
list(APPEND sources ${sub_sources})
elseif(child MATCHES "\\.cpp$|\\.c$|\\.h$")
list(APPEND sources ${full_path})
endif()
endforeach()
set(${result_var} ${sources} PARENT_SCOPE)
endfunction()
collect_sources(${CMAKE_SOURCE_DIR} all_sources)
message("Found ${all_sources}")
警告:递归深度不要太大。CMake的递归深度默认有限制,太深了会报 CMake Error: Recursion depth exceeded。我曾经在一个嵌套了20层的目录结构上跑递归,直接崩了。后来改用 file(GLOB_RECURSE) 才搞定。
知识体系总览
下面这张图把本章的核心逻辑串起来了,你可以对照着回顾一下:
这张图把函数和宏的核心知识点都串起来了。你重点关注两个地方:一是 function 和 macro 的作用域差异,二是参数处理的那几个特殊变量。搞懂了这些,写CMake脚本就会顺手很多。
好了,函数和宏这块就聊到这儿。记住我那句忠告:能用函数就别用宏,除非你真的需要宏那种“直接注入”的效果。下一章咱们聊聊CMake的模块系统,那又是另一番天地了。
公众号:蓝海资料掘金营,微信deep3321