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(),除非你明确需要宏的“穿透”效果。

⚠️ 我曾经踩过的坑:在一个大项目里,用宏定义了一个“临时变量”辅助函数,结果这个变量在宏结束后依然存在,污染了后续的 CMake 逻辑。排查了整整一个下午才发现是宏的作用域问题。从那以后,我给自己定了个规矩:能用 function() 就别用 macro()

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 就被“覆盖”了。这就是宏的 参数名冲突 问题。

💡 我的建议:在宏内部,尽量用 ${ARGV0}、${ARGV1} 来访问参数,而不是用命名参数。这样可以避免变量名冲突。当然,更好的做法是直接用 function()。

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() vs macro() 核心逻辑 CMake 代码复用 function() — 独立作用域 macro() — 调用者作用域 ✅ 内部变量不影响外部 ✅ 支持 return() 提前退出 ✅ 参数名无冲突风险 ⚠️ 返回变量需 PARENT_SCOPE ⚠️ 内部变量直接修改外部 ❌ 不支持 return() ⚠️ 参数名可能冲突 ✅ 性能略高(直接展开) 💡 建议:默认用 function(),特殊场景用 macro()
💡 实用技巧:如果你不确定该用 function() 还是 macro(),我的经验是——先写 function()。等你发现确实需要修改调用者的变量,或者对性能有极致要求时,再改成 macro()。这样能避免 90% 的“变量污染”问题。

好了,关于 function() 和 macro() 的核心内容就这些。记住一句话:function() 是“隔离”的,macro() 是“穿透”的。理解了这个本质,你就能在项目中游刃有余地选择和使用它们了。


公众号:蓝海资料掘金营,微信deep3321