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的核心知识点串起来了:
16.6 几个实用技巧
最后分享几个我常用的技巧:
- 用function封装add_executable/add_library:统一添加编译选项、链接库,避免每个目标都写一遍
- ARGV遍历:用
foreach(arg IN LISTS ARGV)可以遍历所有参数,适合处理不定长参数列表 - 命名参数+ARGN组合:固定参数用命名方式,可变参数用ARGN,代码可读性最好
- 避免在macro里修改调用者的变量:除非你明确知道自己在做什么,否则很容易出bug
function和macro是CMake模块化的基础。掌握了它们,你的CMakeLists.txt就能从“一锅粥”变成“整洁的代码”。下次遇到重复的CMake代码,别复制粘贴了,封装起来吧。