8. 函数与宏:function命令、macro命令、参数处理、返回值、函数重载、递归函数

说到CMake的函数和宏,我得先坦白一件事。我刚开始用CMake那会儿,总觉得这玩意儿就是个“高级点的配置文件”,直到项目规模膨胀到几十个模块,我才意识到——没有函数和宏,你根本活不下去。

说白了,函数和宏就是CMake里的“代码复用利器”。它们能帮你把重复的逻辑封装起来,让构建脚本变得清爽、可维护。今天咱们就把这块彻底讲透。

8.1 function 与 macro:看似相似,实则不同

CMake里定义可复用代码块有两种方式:function()macro()。语法上几乎一模一样,但行为上有本质区别。

# 函数定义
function(my_function arg1 arg2)
    message("arg1 = ${arg1}, arg2 = ${arg2}")
    set(result "hello" PARENT_SCOPE)
endfunction()

# 宏定义
macro(my_macro arg1 arg2)
    message("arg1 = ${arg1}, arg2 = ${arg2}")
    set(result "hello")
endmacro()

嗯,这里要注意:函数有自己的作用域,宏没有。什么意思呢?

  • 函数内部用 set() 设置的变量,默认只在函数内部可见。想往外传?必须加 PARENT_SCOPE
  • 宏内部用 set() 设置的变量,直接就在调用者的作用域里生效了。

我在项目中遇到过好几次这样的坑:用宏封装了一个设置变量的逻辑,结果调用后发现调用者的变量被意外覆盖了。排查了半天,最后发现是宏的“副作用”。从那以后,我个人的习惯是:能用函数就别用宏,除非你真的需要宏那种“直接注入”的效果。

核心区别总结

特性functionmacro
作用域独立作用域调用者作用域
变量传递需 PARENT_SCOPE直接可见
参数处理值传递(副本)文本替换(展开)
推荐场景大多数情况需要修改调用者变量

8.2 参数处理:别让参数把你绕晕

CMake的参数处理其实挺灵活的。除了普通的命名参数,还有几个特殊变量你得记住:

  • ARGC:参数个数
  • ARGV:所有参数列表
  • ARGN:超出命名参数的部分
  • ARGV0ARGV1...:按索引访问
function(print_args first second)
    message("first = ${first}")
    message("second = ${second}")
    message("total args = ${ARGC}")
    message("all args = ${ARGV}")
    message("extra args = ${ARGN}")
endfunction()

print_args(a b c d e)
# 输出:
# first = a
# second = b
# total args = 5
# all args = a;b;c;d;e
# extra args = c;d;e

你想想看,如果参数数量不确定,ARGN 就派上大用场了。我曾经写过一个批量添加测试用例的函数,就是用 ARGN 接收不定数量的源文件,然后逐个注册测试。

小技巧:处理参数时,建议先用 cmake_parse_arguments() 做解析,尤其是参数里有可选参数、关键字参数的时候。这比手动解析 ARGV 要靠谱得多。

8.3 返回值:函数怎么把结果传出来?

函数没有 return 语句返回值这一说。那怎么把结果传出来?三种方式:

  1. 设置变量并加 PARENT_SCOPE:最常用
  2. 设置全局属性:用 set_property(GLOBAL ...)
  3. 通过输出参数:把变量名作为参数传进去,函数内部修改它
# 方式1:PARENT_SCOPE
function(add_one value result_var)
    math(EXPR result "${value} + 1")
    set(${result_var} ${result} PARENT_SCOPE)
endfunction()

add_one(5 output)
message("output = ${output}")  # 输出:output = 6

# 方式2:输出参数
function(get_platform out_var)
    if(WIN32)
        set(${out_var} "Windows" PARENT_SCOPE)
    elseif(APPLE)
        set(${out_var} "macOS" PARENT_SCOPE)
    else()
        set(${out_var} "Linux" PARENT_SCOPE)
    endif()
endfunction()

get_platform(platform)
message("platform = ${platform}")

我个人比较喜欢第二种方式——把变量名作为参数传进去。这样调用方一看就知道这个参数是用来接收结果的,语义清晰。

8.4 函数重载?不存在的,但有替代方案

CMake不支持函数重载。你定义两个同名函数,后面的会覆盖前面的。那怎么办?

我常用的做法是:用关键字参数模拟重载。通过 cmake_parse_arguments() 判断传入了哪些参数,走不同的逻辑分支。

function(configure_target target)
    set(options STATIC SHARED)
    set(oneValueArgs OUTPUT_NAME)
    set(multiValueArgs SOURCES DEPENDENCIES)
    cmake_parse_arguments(PARSE_ARGV 1 "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})

    if(PARSE_STATIC)
        add_library(${target} STATIC)
    elseif(PARSE_SHARED)
        add_library(${target} SHARED)
    else()
        add_executable(${target})
    endif()

    if(PARSE_SOURCES)
        target_sources(${target} PRIVATE ${PARSE_SOURCES})
    endif()
    if(PARSE_DEPENDENCIES)
        target_link_libraries(${target} PRIVATE ${PARSE_DEPENDENCIES})
    endif()
    if(PARSE_OUTPUT_NAME)
        set_target_properties(${target} PROPERTIES OUTPUT_NAME ${PARSE_OUTPUT_NAME})
    endif()
endfunction()

# 调用时就像重载一样灵活
configure_target(my_lib STATIC SOURCES a.cpp b.cpp)
configure_target(my_app SOURCES main.cpp DEPENDENCIES my_lib)

你看,一个函数处理了静态库、动态库、可执行文件三种情况。这就是CMake里的“伪重载”。

8.5 递归函数:小心别把栈搞炸了

CMake支持递归调用。但说实话,我在实际项目中很少用。为什么?因为CMake的递归效率不高,而且容易把调用栈搞炸。

不过有些场景确实适合递归,比如遍历目录树:

function(collect_sources dir result_var)
    file(GLOB children RELATIVE ${dir} ${dir}/*)
    set(sources "")
    foreach(child ${children})
        set(full_path "${dir}/${child}")
        if(IS_DIRECTORY ${full_path})
            # 递归调用
            collect_sources(${full_path} sub_sources)
            list(APPEND sources ${sub_sources})
        elseif(child MATCHES "\\.cpp$|\\.c$|\\.h$")
            list(APPEND sources ${full_path})
        endif()
    endforeach()
    set(${result_var} ${sources} PARENT_SCOPE)
endfunction()

collect_sources(${CMAKE_SOURCE_DIR} all_sources)
message("Found ${all_sources}")

警告:递归深度不要太大。CMake的递归深度默认有限制,太深了会报 CMake Error: Recursion depth exceeded。我曾经在一个嵌套了20层的目录结构上跑递归,直接崩了。后来改用 file(GLOB_RECURSE) 才搞定。

知识体系总览

下面这张图把本章的核心逻辑串起来了,你可以对照着回顾一下:

函数与宏 定义方式 function() macro() 参数处理 ARGC/ARGV ARGN 返回值 PARENT_SCOPE 输出参数 高级特性 函数重载(模拟) 递归函数 cmake_parse_arguments 核心:作用域理解 + 参数灵活处理 + 避免递归过深

这张图把函数和宏的核心知识点都串起来了。你重点关注两个地方:一是 functionmacro 的作用域差异,二是参数处理的那几个特殊变量。搞懂了这些,写CMake脚本就会顺手很多。

好了,函数和宏这块就聊到这儿。记住我那句忠告:能用函数就别用宏,除非你真的需要宏那种“直接注入”的效果。下一章咱们聊聊CMake的模块系统,那又是另一番天地了。


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