25、函数与宏:function()和macro()的定义与调用,参数传递
CMake 里写重复逻辑,最直接的办法就是封装。我见过不少项目,CMakeLists.txt 动辄上千行,里面全是重复的 add_library、target_link_libraries,看着就头疼。其实用 function() 或 macro() 就能轻松搞定。
这一讲,咱们就聊聊 CMake 里的函数和宏。说白了,它们就是让你把一段逻辑包起来,起个名字,然后到处调用。但两者有本质区别,用错了会踩坑。
25.1 为什么需要函数和宏?
你想想看,一个项目里可能有十几个子模块,每个模块都要设置编译选项、链接库、安装路径。如果每次都手写一遍,不仅累,还容易漏掉某个参数。封装成函数或宏,一次定义,到处复用。
我在项目中遇到过最典型的场景:团队里每个人写的 CMake 风格都不一样,有人用 set 直接改全局变量,有人到处写 add_definitions。后来我统一封装了几个函数,大家照着用,代码立马清爽了。
25.2 function() 的定义与调用
function() 会创建一个新的作用域。什么意思呢?函数内部定义的变量,默认不会影响外部。这跟 C++ 里的函数很像——局部变量只在函数内有效。
先看个最简单的例子:
function(my_print message)
message(STATUS "【自定义打印】${message}")
endfunction()
my_print("Hello, CMake!")
调用的时候,直接写函数名加参数就行。注意,参数之间用空格分隔,不需要逗号。
函数内部可以通过 ${ARGV} 拿到所有参数,${ARGC} 拿到参数个数。我习惯用 ${ARGV} 来处理不定参数,灵活很多。
关键点:function 内部用 set(var value PARENT_SCOPE) 才能把变量传出去。否则外部看不到。
25.3 macro() 的定义与调用
macro() 就不一样了。它不会创建新作用域,本质上就是文本替换。宏内部的所有变量操作,直接作用在调用者的作用域里。
看个对比:
macro(my_macro message)
set(result "宏处理: ${message}")
endmacro()
my_macro("测试")
message(STATUS "result = ${result}") # 能打印出来
换成 function 试试:
function(my_func message)
set(result "函数处理: ${message}")
endfunction()
my_func("测试")
message(STATUS "result = ${result}") # 打印为空!
为什么会这样?因为函数里的 set 默认只在函数内部生效。想传出来?加 PARENT_SCOPE。
注意:宏里的 return() 行为跟函数不同。宏里的 return() 会直接跳出整个调用链,容易引发意外。我曾经在宏里用 return() 想提前结束,结果把外层的 foreach 也给跳了……
25.4 参数传递的细节
CMake 的参数传递,有几个点容易搞混。我列个表,一目了然:
| 变量 | 含义 | 示例 |
|---|---|---|
${ARGC} |
参数个数 | 3 |
${ARGV} |
所有参数列表 | a;b;c |
${ARGN} |
超出命名参数的部分 | 如果函数定义了两个命名参数,ARGN 就是第三个及以后 |
${ARGV0} ~ ${ARGV<N>} |
按索引访问单个参数 | ${ARGV0} 是第一个参数 |
我个人习惯用 ${ARGN} 来处理可选参数。比如:
function(my_target name)
# 第一个参数是 name,剩下的都当作源文件
set(sources ${ARGN})
add_library(${name} ${sources})
endfunction()
my_target(my_lib a.cpp b.cpp c.cpp)
这样调用起来很自然,不需要把所有源文件都塞到一个列表变量里。
25.5 函数 vs 宏:怎么选?
我个人的经验是:优先用 function。原因很简单——作用域隔离,不容易出幺蛾子。宏虽然写起来省事,但副作用太隐蔽。
什么时候用宏?
- 你需要修改调用者作用域里的变量
- 你封装的是非常简单的文本替换逻辑
- 你明确知道宏的副作用,并且能控制住
举个例子,我有时候用宏来封装一些调试开关:
macro(enable_debug)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DDEBUG -g")
endmacro()
这种场景用宏就很合适——我就是要修改外部的 CMAKE_CXX_FLAGS。
小技巧:如果你不确定用哪个,先写 function。发现需要往外传变量时,再改成 macro 或者用 PARENT_SCOPE。别一开始就上宏,容易失控。
25.6 实战:封装一个通用库加载函数
咱们来写个实际能用的。假设项目里有很多第三方库,每个库都要找头文件、链接库、设置宏定义。封装一下:
function(load_third_party name)
# 参数:name 库名,后面跟可选的 include_dirs lib_dirs
set(one_value_args INCLUDE_DIRS LIB_DIRS)
set(multi_value_args DEFINES)
cmake_parse_arguments(PARSE_ARGV 1 "${name}" "" "${one_value_args}" "${multi_value_args}")
# 找头文件
if(${name}_INCLUDE_DIRS)
include_directories(${${name}_INCLUDE_DIRS})
endif()
# 找库文件
if(${name}_LIB_DIRS)
link_directories(${${name}_LIB_DIRS})
endif()
# 添加宏定义
foreach(def ${${name}_DEFINES})
add_definitions(-D${def})
endforeach()
message(STATUS "已加载第三方库: ${name}")
endfunction()
# 调用示例
load_third_party(OpenCV
INCLUDE_DIRS /usr/local/include/opencv4
LIB_DIRS /usr/local/lib
DEFINES USE_OPENCV OPENCV_VERSION_4
)
这里用了 cmake_parse_arguments,这是 CMake 官方推荐的参数解析方式。支持单值参数和多值参数,调用起来跟系统命令一样清晰。
25.7 避坑指南
最后分享几个我踩过的坑:
- 宏里的 return 会跳出外层:我曾经在宏里写
return()想提前结束,结果把外层的function也给跳了。排查了半天…… - 函数里忘记 PARENT_SCOPE:在函数里
set了一个变量,外面死活拿不到。后来才发现默认只在局部生效。 - 参数名跟变量名冲突:比如函数参数叫
name,内部又用set(name xxx),会把参数值覆盖掉。建议参数名加个前缀,比如_name。 - ARGV 在宏里的行为:宏里
${ARGV}是调用者的参数,不是宏自己的。这个跟函数不一样,容易混淆。
嗯,函数和宏就聊这么多。记住核心区别:函数有作用域,宏没有。选对工具,你的 CMake 代码会干净很多。