6、函数与宏:function()与macro()的定义与区别、参数传递机制、return()的使用、作用域规则
说实话,CMake 里的函数和宏,是我用了好几年才真正搞明白的东西。刚开始写 CMakeLists.txt 时,我总觉得这俩差不多——反正都是把一段逻辑包起来复用嘛。直到有一次,我在一个大型项目里用宏定义了一个“变量设置”的辅助工具,结果变量值到处乱窜,排查了整整一个下午……嗯,从那以后,我就老老实实把 function() 和 macro() 的区别刻在了脑子里。
这一节,我们就来彻底讲清楚:它们到底怎么定义、参数怎么传、return() 怎么用、作用域又有什么门道。
6.1 function() 与 macro() 的定义
先看最基本的定义方式。两者语法几乎一样:
# 定义一个函数
function(my_function arg1 arg2)
message("arg1 = ${arg1}")
message("arg2 = ${arg2}")
endfunction()
# 定义一个宏
macro(my_macro arg1 arg2)
message("arg1 = ${arg1}")
message("arg2 = ${arg2}")
endmacro()
调用方式也完全一样:
my_function(hello world)
my_macro(foo bar)
你看,光从定义和调用上看,根本分不出区别。但实际行为天差地别——核心就在于 作用域。
6.2 核心区别:作用域
这是最要命的一点,也是我当年踩坑的地方。
- function() 有自己的作用域:内部设置的变量、策略等,默认不会影响调用者。
- macro() 没有自己的作用域:它直接在调用者的作用域里展开,相当于把宏体里的代码“粘贴”到调用位置。
看个例子你就明白了:
set(my_var "outside")
function(test_func)
set(my_var "inside_func")
message("在函数内: ${my_var}")
endfunction()
macro(test_macro)
set(my_var "inside_macro")
message("在宏内: ${my_var}")
endmacro()
test_func()
message("调用函数后: ${my_var}") # 输出 "outside"
test_macro()
message("调用宏后: ${my_var}") # 输出 "inside_macro"
为什么会这样?因为函数内部 set() 默认只影响函数自己的作用域,而宏的 set() 直接修改了调用者的变量。我个人习惯是:只要涉及变量设置,优先用 function(),除非你明确需要宏的“穿透”效果。
6.3 参数传递机制
CMake 的参数传递其实挺灵活的,但也有些“坑”。
6.3.1 命名参数
就像上面那样,定义时写 arg1 arg2,调用时按位置传:
function(print_info name version)
message("名称: ${name}, 版本: ${version}")
endfunction()
print_info("MyLib" 2.0)
6.3.2 可变参数:ARGV / ARGN
有时候你不想限制参数个数。CMake 提供了几个内置变量:
- ARGC:参数总数
- ARGV:所有参数的列表
- ARGN:超出命名参数之外的参数列表
- ARGV0, ARGV1, ...:按索引访问
function(collect_items first)
message("第一个参数: ${first}")
message("其余参数: ${ARGN}")
message("所有参数: ${ARGV}")
message("参数个数: ${ARGC}")
endfunction()
collect_items(a b c d)
# 输出:
# 第一个参数: a
# 其余参数: b;c;d
# 所有参数: a;b;c;d
# 参数个数: 4
这里有个细节:ARGV 包含所有参数(包括命名参数),而 ARGN 只包含“多出来的”参数。你想想看,如果你定义了一个函数有 3 个命名参数,但调用时传了 5 个,那 ARGN 就是第 4、第 5 个参数。
6.3.3 参数传递的“陷阱”
在宏里面,参数传递有个很隐蔽的问题——参数名和变量名可能冲突。看这个例子:
set(name "全局名字")
macro(show_name name)
message("传入的 name: ${name}")
endmacro()
show_name("局部名字")
# 输出: 传入的 name: 全局名字
等等,怎么会这样?因为宏没有自己的作用域,它直接展开到调用者的作用域里。而调用者作用域里已经有一个 name 变量了,宏的参数 name 就被“覆盖”了。这就是宏的 参数名冲突 问题。
6.4 return() 的使用
return() 在 CMake 里有点特殊——它只能用在 function() 内部,不能用在 macro() 里。为什么?因为宏是“展开”的,没有自己的调用栈,return() 没有意义。
function(check_version ver)
if(ver VERSION_LESS "3.0")
message("版本太低,退出")
return()
endif()
message("版本检查通过: ${ver}")
endfunction()
check_version("2.8")
# 输出: 版本太低,退出
# 不会输出 "版本检查通过"
如果你在宏里用 return(),CMake 会报错。嗯,这个我试过,确实不行。
另外,return() 还可以带一个返回值——不过 CMake 函数没有“返回值”的概念,你只能通过设置变量来“返回”:
function(compute_sum a b result_var)
math(EXPR sum "${a} + ${b}")
set(${result_var} ${sum} PARENT_SCOPE)
endfunction()
compute_sum(3 5 result)
message("3 + 5 = ${result}") # 输出: 3 + 5 = 8
注意这里用了 PARENT_SCOPE,因为函数内部 set() 默认只影响函数自己的作用域。想“返回”值给调用者,必须显式指定 PARENT_SCOPE。
6.5 作用域规则总结
我把作用域规则整理成了一张表,方便你对照:
| 特性 | function() | macro() |
|---|---|---|
| 是否有独立作用域 | ✅ 是 | ❌ 否(在调用者作用域展开) |
| 内部 set() 是否影响调用者 | ❌ 默认不影响(需 PARENT_SCOPE) | ✅ 直接影响 |
| return() 是否可用 | ✅ 可用 | ❌ 不可用 |
| 参数名冲突风险 | 低(有独立作用域) | 高(直接展开) |
| 性能 | 略低(有调用开销) | 略高(直接展开) |
| 推荐使用场景 | 大多数情况 | 需要修改调用者变量、性能敏感 |
6.6 知识体系图
下面这张图帮你理清 function() 和 macro() 的核心逻辑:
好了,关于 function() 和 macro() 的核心内容就这些。记住一句话:function() 是“隔离”的,macro() 是“穿透”的。理解了这个本质,你就能在项目中游刃有余地选择和使用它们了。