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=hellob=worldextra被忽略了。但如果你想知道所有传入的参数,可以用${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.cpputils.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 知识体系总览

下面这张图帮你理清函数和宏的核心逻辑:

CMake 函数与宏知识体系 function() 函数 macro() 宏 特性 • 拥有独立变量作用域 • 修改外部变量需 PARENT_SCOPE • 参数作为局部变量传递 • 推荐用于大多数场景 • 避免变量污染 特性 • 文本替换,无独立作用域 • 直接修改调用者变量 • 参数即文本替换 • 性能略优(无函数调用开销) • 小心参数名与变量名冲突

10.7 实战建议

最后,分享几个我实际项目中的经验:

  • 优先用函数:除非你明确需要修改调用者的变量,否则函数更安全。
  • 参数命名要清晰:宏的参数名最好加个前缀,比如_name,避免和外部变量冲突。
  • 善用ARGN:写通用工具函数时,用${ARGN}透传多余参数,灵活性高很多。
  • 调试时加message:函数或宏不按预期工作时,在内部加message()打印变量值,很快就能定位问题。

核心总结:函数有作用域隔离,安全但需显式传值;宏是文本替换,灵活但容易污染变量。记住这个区别,你就能在CMake里写出干净、可复用的代码。


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