9、函数与宏:function定义、macro定义、参数处理(ARGC/ARGV/ARGN)、return命令
说到CMake里的函数和宏,我刚开始接触时也觉得有点绕。毕竟写C++的人习惯了函数就是函数,宏就是宏,但CMake里这两者长得像,用起来却各有脾气。今天咱们就把它们彻底捋清楚。
9.1 为什么需要函数和宏?
你想想看,一个项目里如果到处是重复的add_library、target_link_libraries,那维护起来得多痛苦。函数和宏就是用来封装这些重复逻辑的。我在一个嵌入式项目里,光不同芯片的编译选项就写了十几遍,后来封装成一个函数,清爽多了。
9.2 function 定义
function会创建一个新的作用域。什么意思呢?就是函数内部定义的变量,外面看不到。除非你用set(... PARENT_SCOPE)显式传出去。
function(my_add_library target_name)
add_library(${target_name} STATIC ${ARGN})
target_include_directories(${target_name} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
message(STATUS "已创建库: ${target_name}")
endfunction()
# 调用
my_add_library(mylib src1.cpp src2.cpp)
这里${ARGN}接收了额外的源文件参数。我个人习惯把函数名用项目名做前缀,避免跟CMake内置函数冲突。
set(var value PARENT_SCOPE)。
9.3 macro 定义
macro就不一样了。它本质上是个文本替换,没有独立作用域。宏里定义的变量,直接就在调用者的作用域里生效。
macro(my_print_libs)
message(STATUS "当前所有库目标:")
get_property(libs DIRECTORY PROPERTY BUILDSYSTEM_TARGETS)
foreach(lib ${libs})
message(STATUS " - ${lib}")
endforeach()
endmacro()
嗯,这里要注意:宏里的${lib}变量在宏结束后依然存在。我曾经因为这个踩过坑——宏里循环用的临时变量,把外面同名的变量给覆盖了。所以用宏的时候,变量命名要格外小心。
9.4 function vs macro:到底选哪个?
| 对比项 | function | macro |
|---|---|---|
| 作用域 | 独立作用域 | 调用者作用域 |
| 变量隔离 | 是 | 否 |
| 性能 | 略慢(有调用开销) | 快(文本替换) |
| 推荐场景 | 需要封装逻辑、避免变量污染 | 简单快捷的代码复用 |
我个人建议:除非你很清楚为什么需要宏,否则一律用function。宏的隐式变量传递太容易出问题了。
9.5 参数处理:ARGC / ARGV / ARGN
这三个变量是CMake自动帮你生成的,用来处理函数或宏的参数。
- ARGC:参数总数
- ARGV:所有参数的列表
- ARGN:超出命名参数之外的参数列表
举个例子你就明白了:
function(show_args first second)
message("ARGC = ${ARGC}")
message("ARGV = ${ARGV}")
message("ARGN = ${ARGN}")
message("first = ${first}, second = ${second}")
endfunction()
show_args(a b c d e)
# 输出:
# ARGC = 5
# ARGV = a;b;c;d;e
# ARGN = c;d;e
# first = a, second = b
说白了,ARGN就是「多出来的那些参数」。我在写一个支持可变源文件的库封装函数时,ARGN简直是救星。
9.6 参数解析进阶:cmake_parse_arguments
当参数多了,光靠ARGV就不够用了。CMake提供了cmake_parse_arguments,支持命名参数、可选参数、列表参数。
function(my_complex_func)
set(options VERBOSE QUIET)
set(oneValueArgs TARGET OUTPUT_DIR)
set(multiValueArgs SOURCES DEPENDS)
cmake_parse_arguments(MY "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if(MY_VERBOSE)
message(STATUS "目标: ${MY_TARGET}")
message(STATUS "源文件: ${MY_SOURCES}")
endif()
add_executable(${MY_TARGET} ${MY_SOURCES})
target_link_libraries(${MY_TARGET} ${MY_DEPENDS})
endfunction()
my_complex_func(
TARGET myapp
SOURCES main.cpp utils.cpp
DEPENDS fmt spdlog
VERBOSE
)
这个函数我几乎每个项目都在用。尤其是multiValueArgs,处理源文件列表特别方便。
前缀_参数名 的形式生成变量。前缀可以任意取,但建议跟函数名相关。
9.7 return 命令
return()可以提前退出函数或宏。注意,它不能返回值——CMake函数没有返回值这个概念。想「返回」数据,得通过set(... PARENT_SCOPE)或者修改全局变量。
function(find_or_create_library name)
if(TARGET ${name})
message(STATUS "库 ${name} 已存在,跳过")
return()
endif()
add_library(${name} STATIC IMPORTED)
message(STATUS "已创建导入库: ${name}")
endfunction()
我在CI脚本里经常用return()做条件跳过。比如检测到某些工具链不支持时,直接返回,避免后续编译报错。
9.8 知识体系总览
下面这张图帮你理清函数和宏的核心关系:
9.9 避坑指南
最后分享几个我实际踩过的坑:
- 宏里的 return 会退出调用者——我曾经在宏里写了个条件 return,结果调用它的函数也提前结束了,排查了半天。
- ARGN 只包含未命名的参数——如果你把所有参数都命名了,ARGN 就是空的。
- cmake_parse_arguments 必须在函数/宏内部调用——不能在全局作用域用。
- 函数内 set 变量默认不对外生效——记得加
PARENT_SCOPE。
嗯,掌握了这些,CMake里的代码复用你就基本拿下了。函数和宏用得好,CMakeLists.txt 能瘦身一大半,维护起来也舒心得多。
公众号:蓝海资料掘金营,微信deep3321