10、函数与宏:function()定义函数,macro()定义宏
说实话,我刚接触CMake那会儿,最头疼的就是代码复用。一个项目里几十个目标,每个都要写一遍target_link_libraries?那也太原始了。后来我发现了function()和macro(),才算是真正入了门。
这一节,咱们就聊聊这两个东西。说白了,它们就是CMake里的“子程序”。你写一次,到处调用,省时省力。
10.1 function() 和 macro() 的基本用法
先看个最简单的例子。假设你要给多个目标添加相同的编译选项:
function(add_my_flags target)
target_compile_options(${target} PRIVATE -Wall -Wextra)
endfunction()
add_my_flags(my_app)
add_my_flags(my_lib)
嗯,就这么简单。function()和endfunction()之间就是函数体。调用时传参,函数内部用${target}接收。
那macro()呢?语法几乎一模一样:
macro(add_my_flags_macro target)
target_compile_options(${target} PRIVATE -Wall -Wextra)
endmacro()
add_my_flags_macro(my_app)
你可能会问:“这俩有啥区别?” 别急,我当年也困惑过。咱们往下看。
10.2 参数传递:别漏了ARGN
函数和宏都支持参数。但有个坑——参数个数不匹配时怎么办?
比如你定义了一个函数,接受两个参数,但调用时传了三个:
function(print_two a b)
message("a=${a}, b=${b}")
endfunction()
print_two(hello world extra)
结果呢?a=hello,b=world,extra被忽略了。但如果你想知道所有传入的参数,可以用${ARGN}:
function(print_all a b)
message("a=${a}, b=${b}")
message("extra args: ${ARGN}")
endfunction()
print_all(hello world extra more)
# 输出:
# a=hello, b=world
# extra args: extra;more
我个人习惯在写通用工具函数时,总是用${ARGN}把多余参数透传下去。比如:
function(wrap_add_executable name)
add_executable(${name} ${ARGN})
target_compile_options(${name} PRIVATE -O2)
endfunction()
wrap_add_executable(my_app main.cpp utils.cpp)
这样main.cpp和utils.cpp就通过${ARGN}传给了add_executable。
10.3 返回值:别指望return
CMake的函数没有return语句返回值。那怎么把结果传出来?
答案是:通过变量。用set()或者list()修改父作用域的变量。
看个例子:
function(get_os_name result)
if(WIN32)
set(${result} "Windows" PARENT_SCOPE)
elseif(APPLE)
set(${result} "macOS" PARENT_SCOPE)
else()
set(${result} "Linux" PARENT_SCOPE)
endif()
endfunction()
get_os_name(my_os)
message("OS: ${my_os}")
注意那个PARENT_SCOPE。没有它,变量只在函数内部生效,调用方拿不到。我在项目中就吃过这个亏——写了个函数死活传不出结果,查了半天才发现忘了加PARENT_SCOPE。
PARENT_SCOPE。因为宏是直接展开到调用处的,它修改的变量天然就在调用者的作用域里。
10.4 作用域:函数和宏最大的不同
这是最核心的区别。我建议你仔细看。
| 特性 | function() | macro() |
|---|---|---|
| 作用域 | 有自己的变量作用域 | 在调用者作用域中展开 |
| 变量修改 | 需加 PARENT_SCOPE | 直接修改调用者变量 |
| 参数 | 作为局部变量 | 作为字符串替换 |
| 性能 | 略有开销 | 无额外开销 |
举个例子你就明白了:
set(my_var "global")
function(test_func)
set(my_var "function_local")
message("inside func: ${my_var}")
endfunction()
macro(test_macro)
set(my_var "macro_local")
message("inside macro: ${my_var}")
endmacro()
test_func()
message("after func: ${my_var}")
test_macro()
message("after macro: ${my_var}")
输出结果:
inside func: function_local
after func: global
inside macro: macro_local
after macro: macro_local
看到了吗?函数内部修改的变量不影响外部。而宏直接覆盖了外部的my_var。
为什么会这样?因为宏本质上是文本替换。你写的宏代码,在预处理阶段就被原封不动地粘贴到了调用位置。所以它没有自己的作用域。
10.5 避坑指南:宏的参数陷阱
我曾经被宏的参数坑过一次。看这个:
macro(print_var varname)
message("${varname} = ${${varname}}")
endmacro()
set(foo "hello")
print_var(foo)
输出是foo = hello。没问题对吧?
但如果我把参数名和变量名搞混了呢?
macro(print_var foo)
message("${foo} = ${${foo}}")
endmacro()
set(bar "world")
print_var(bar)
输出是bar = world。嗯,也对了。但如果你在宏内部又定义了一个叫foo的变量呢?
macro(print_var foo)
set(foo "oops")
message("${foo} = ${${foo}}")
endmacro()
set(bar "world")
print_var(bar)
message("bar after macro: ${bar}")
输出:
bar = oops
bar after macro: oops
看到了吗?宏内部的set(foo "oops")把外部的bar给改了!因为宏的参数foo在展开时被替换成了bar,所以set(foo "oops")实际上变成了set(bar "oops")。
这就是宏的“参数即文本替换”特性带来的副作用。函数就不会有这个问题,因为函数参数是局部变量。
10.6 知识体系总览
下面这张图帮你理清函数和宏的核心逻辑:
10.7 实战建议
最后,分享几个我实际项目中的经验:
- 优先用函数:除非你明确需要修改调用者的变量,否则函数更安全。
- 参数命名要清晰:宏的参数名最好加个前缀,比如
_name,避免和外部变量冲突。 - 善用ARGN:写通用工具函数时,用
${ARGN}透传多余参数,灵活性高很多。 - 调试时加message:函数或宏不按预期工作时,在内部加
message()打印变量值,很快就能定位问题。
核心总结:函数有作用域隔离,安全但需显式传值;宏是文本替换,灵活但容易污染变量。记住这个区别,你就能在CMake里写出干净、可复用的代码。
公众号:蓝海资料掘金营,微信deep3321