16、CMake函数与宏:function与macro、参数处理(ARGC, ARGV, ARGN)、return

说到CMake里的函数和宏,我刚开始接触时也觉得有点绕。毕竟CMake不是真正的编程语言,但它又确实提供了这些“类编程”的能力。说白了,function和macro就是让你把重复的CMake代码打包起来,一次定义,到处调用。我在做嵌入式项目时,经常要配置不同芯片的编译选项,没有这俩东西,那CMakeLists.txt得写成什么样,想想都头疼。

16.1 function与macro的区别

先看个最简单的例子,感受一下:

# 定义一个宏
macro(print_hello_macro)
    message("Hello from macro")
    set(result "macro_result")
endmacro()

# 定义一个函数
function(print_hello_func)
    message("Hello from function")
    set(result "func_result")
endfunction()

# 调用
print_hello_macro()
message("After macro: result = ${result}")

print_hello_func()
message("After function: result = ${result}")

猜猜输出是什么?

Hello from macro
After macro: result = macro_result
Hello from function
After function: result = 

看到了吗?macro里set的变量,调用完后还在。function里set的变量,调用完就没了。这就是两者最核心的区别——作用域

核心区别一句话总结:

  • macro:代码直接展开到调用处,共享调用者的变量作用域
  • function:创建新的变量作用域,内部变量默认不对外可见

我个人习惯是:能用function就别用macro。为什么?因为macro的“代码展开”机制容易引发变量污染。我曾经在一个项目里,就因为macro里不小心改了个变量名,结果跟外层的变量重名了,排查了整整一下午。function有独立作用域,相当于给你加了一层保护。

16.2 参数处理:ARGC、ARGV、ARGN

不管是function还是macro,传参的方式是一样的。CMake提供了三个内置变量来处理参数:

变量含义
ARGC传入的参数个数
ARGV所有参数的列表(包括命名参数)
ARGN超出命名参数之外的剩余参数列表

看个例子就明白了:

function(my_func arg1 arg2)
    message("ARGC = ${ARGC}")
    message("ARGV = ${ARGV}")
    message("ARGN = ${ARGN}")
    message("arg1 = ${arg1}")
    message("arg2 = ${arg2}")
endfunction()

my_func(hello world foo bar baz)

输出:

ARGC = 5
ARGV = hello;world;foo;bar;baz
ARGN = foo;bar;baz
arg1 = hello
arg2 = world

嗯,这里要注意:ARGV包含所有参数,ARGN只包含“多出来”的那些。这个设计其实很实用。比如你写一个函数,前两个参数是固定的,后面可以跟任意多个源文件:

function(add_sources target lib_type)
    # target和lib_type是固定参数
    # 剩下的都是源文件
    set(sources ${ARGN})
    
    if(${lib_type} STREQUAL "STATIC")
        add_library(${target} STATIC ${sources})
    else()
        add_library(${target} SHARED ${sources})
    endif()
endfunction()

# 调用:前两个是固定参数,后面全是源文件
add_sources(my_lib STATIC src/a.c src/b.c src/c.c)

我在项目中经常用这个模式,尤其是当源文件数量不确定时,ARGN简直就是救星。

16.3 return():提前退出

CMake的return()命令,可以在function或macro中提前返回。注意,它不能返回值,只是“停止执行”。

function(check_compiler)
    if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
        message("GCC detected")
        return()
    endif()
    
    if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
        message("Clang detected")
        return()
    endif()
    
    message("Unknown compiler: ${CMAKE_CXX_COMPILER_ID}")
endfunction()

这里有个坑:在macro里用return(),行为跟function不一样。因为macro是代码展开,return()会直接跳出macro所在的整个上下文。我曾经在macro里用return(),结果把调用它的父函数也给跳出了,那叫一个酸爽。

避坑指南:

  • function里的return():只退出当前函数
  • macro里的return():退出调用macro的那个上下文(可能是函数、文件等)
  • 所以,尽量在function里用return(),macro里慎用

16.4 实战:封装一个日志函数

说了这么多,来点实际的。我在嵌入式项目里经常需要控制日志输出,调试时打开,发布时关闭。用function封装一下:

# 全局开关
set(ENABLE_DEBUG ON)

function(debug_log msg)
    if(NOT ENABLE_DEBUG)
        return()
    endif()
    
    message(STATUS "[DEBUG] ${msg}")
endfunction()

# 使用
debug_log("Starting build process...")
debug_log("Target platform: ${CMAKE_SYSTEM_NAME}")

ENABLE_DEBUG为OFF时,所有debug_log调用直接跳过,不影响性能。这个模式我在多个项目里复用,简单又有效。

16.5 知识体系总览

下面这张图,把function和macro的核心知识点串起来了:

CMake函数与宏知识体系 function() macro() 特性 • 独立变量作用域 • 内部set不影响外部 • return()安全退出 特性 • 代码直接展开 • 共享调用者作用域 • return()需谨慎 参数处理:ARGC / ARGV / ARGN ARGC 参数个数 ARGV 所有参数列表 ARGN 剩余参数列表

16.6 几个实用技巧

最后分享几个我常用的技巧:

  • 用function封装add_executable/add_library:统一添加编译选项、链接库,避免每个目标都写一遍
  • ARGV遍历:用foreach(arg IN LISTS ARGV)可以遍历所有参数,适合处理不定长参数列表
  • 命名参数+ARGN组合:固定参数用命名方式,可变参数用ARGN,代码可读性最好
  • 避免在macro里修改调用者的变量:除非你明确知道自己在做什么,否则很容易出bug

function和macro是CMake模块化的基础。掌握了它们,你的CMakeLists.txt就能从“一锅粥”变成“整洁的代码”。下次遇到重复的CMake代码,别复制粘贴了,封装起来吧。


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