6、CMake函数与宏:function()定义函数、macro()定义宏、参数传递、返回值处理
写CMakeLists.txt写到一定规模,你就会发现——代码开始重复了。同样的查找库、同样的编译选项设置、同样的安装逻辑,到处复制粘贴。这时候,你就需要函数和宏了。
我个人习惯,只要某个逻辑出现两次以上,就封装起来。别偷懒,后期维护会感谢你的。
6.1 函数 vs 宏:到底有什么区别?
很多初学者搞不清function()和macro()的区别。说白了,它们都能封装代码,但作用域机制完全不同。
| 特性 | function() | macro() |
|---|---|---|
| 作用域 | 创建新作用域 | 在调用者作用域展开 |
| 变量修改 | 不影响调用者(除非用PARENT_SCOPE) | 直接修改调用者变量 |
| 参数处理 | 参数是真正的变量 | 参数是字符串替换 |
| return() | 支持 | 不支持 |
嗯,这里要注意:宏不是函数。宏本质上是文本替换,就像C语言的#define。而函数有独立的作用域。
核心原则:需要隔离副作用用function(),需要修改调用者变量用macro()。我个人90%的场景都用function()。
6.2 定义函数:function() 基础用法
先看一个最简单的例子:
function(my_add_library target_name)
add_library(${target_name} STATIC ${ARGN})
target_include_directories(${target_name} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
endfunction()
# 调用
my_add_library(mylib src1.cpp src2.cpp)
这里有几个关键点:
- ${ARGN}:接收所有额外参数。上面例子中,src1.cpp和src2.cpp就通过ARGN传进去了。
- 参数按位置匹配:target_name对应第一个参数。
- 函数名大小写不敏感:但建议统一用小写。
我在项目中遇到过一个问题:有人把函数名写成my_Add_Library,调用时又写成my_add_library,虽然能工作,但代码风格很混乱。建议团队统一规范。
6.3 参数传递的几种方式
CMake的参数传递比你想的灵活。我总结了三种常用模式:
6.3.1 位置参数
function(print_info name version)
message("Name: ${name}")
message("Version: ${version}")
endfunction()
print_info("MyProject" "1.0.0")
最直观,但参数多了容易搞混顺序。
6.3.2 关键字参数
function(configure_app)
set(options STATIC SHARED)
set(oneValueArgs NAME VERSION)
set(multiValueArgs SOURCES DEPENDENCIES)
cmake_parse_arguments(PARSE "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if(PARSE_STATIC)
message("Building static library")
endif()
message("App name: ${PARSE_NAME}")
endfunction()
configure_app(
NAME MyApp
VERSION 2.0
SOURCES main.cpp utils.cpp
STATIC
)
cmake_parse_arguments是处理复杂参数的利器。options是开关选项,oneValueArgs是单值参数,multiValueArgs是多值参数。
我的习惯:只要参数超过3个,就用关键字参数。代码可读性提升一个档次。
6.3.3 可变参数
function(collect_sources)
set(sources "")
foreach(arg IN LISTS ARGN)
file(GLOB_RECURSE matched "${arg}/*.cpp")
list(APPEND sources ${matched})
endforeach()
set(sources ${sources} PARENT_SCOPE)
endfunction()
collect_sources(src1 src2 src3)
message("Sources: ${sources}")
ARGC和ARGV可以获取参数总数和所有参数列表,但日常用ARGN就够了。
6.4 返回值处理:不止一种方式
函数怎么返回值?这是新手问得最多的问题。CMake函数没有return value的概念,但有几种替代方案。
6.4.1 通过变量返回
function(get_platform)
if(WIN32)
set(result "Windows" PARENT_SCOPE)
elseif(APPLE)
set(result "macOS" PARENT_SCOPE)
else()
set(result "Linux" PARENT_SCOPE)
endif()
endfunction()
get_platform()
message("Platform: ${result}")
注意PARENT_SCOPE关键字。没有它,result只在函数内部有效。
6.4.2 通过输出参数返回
function(find_python out_var)
find_program(python_path python3)
if(python_path)
set(${out_var} "${python_path}" PARENT_SCOPE)
else()
set(${out_var} "NOT_FOUND" PARENT_SCOPE)
endif()
endfunction()
find_python(my_python)
message("Python path: ${my_python}")
这种模式更灵活,调用者可以指定变量名。
6.4.3 使用return()提前退出
function(validate_version version)
if(NOT version)
message(FATAL_ERROR "Version is required")
return()
endif()
if(version VERSION_LESS "1.0")
message(WARNING "Version too old: ${version}")
return()
endif()
message("Version ${version} is valid")
endfunction()
return()只能用在function()中,macro()里不能用。我曾经在宏里用了return(),结果整个CMakeLists.txt都退出了——血的教训。
注意:macro()中的return()会直接返回调用者,而不是宏本身。所以如果你在宏里写return(),相当于在调用宏的地方执行了return()。
6.5 宏的特殊行为:文本替换的陷阱
宏看起来和函数很像,但行为完全不同。看这个例子:
macro(print_var var_name)
message("${var_name} = ${${var_name}}")
endmacro()
set(my_var "hello")
print_var(my_var) # 输出: my_var = hello
宏里${var_name}会被直接替换成my_var,然后${my_var}再被展开成hello。这就是文本替换的威力。
但陷阱也在这里:
macro(bad_macro)
set(x 100)
endmacro()
set(x 1)
bad_macro()
message("x = ${x}") # 输出: x = 100
宏修改了调用者的变量!这在函数里不会发生。
避坑指南:我曾经在宏里定义了一个临时变量,结果和调用者的变量重名了,排查了整整两个小时。后来我定了个规矩:宏内部变量全部加_M_前缀。
6.6 实战:封装一个通用安装函数
把前面学的串起来,写一个实用的安装函数:
function(install_target target_name)
set(options EXCLUDE_FROM_ALL)
set(oneValueArgs DESTINATION COMPONENT)
set(multiValueArgs FILES HEADERS)
cmake_parse_arguments(INSTALL "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
# 安装目标文件
install(TARGETS ${target_name}
RUNTIME DESTINATION ${INSTALL_DESTINATION}/bin
LIBRARY DESTINATION ${INSTALL_DESTINATION}/lib
ARCHIVE DESTINATION ${INSTALL_DESTINATION}/lib
)
# 安装头文件
if(INSTALL_HEADERS)
install(FILES ${INSTALL_HEADERS}
DESTINATION ${INSTALL_DESTINATION}/include
)
endif()
# 安装额外文件
if(INSTALL_FILES)
install(FILES ${INSTALL_FILES}
DESTINATION ${INSTALL_DESTINATION}/share
)
endif()
# 组件支持
if(INSTALL_COMPONENT)
set_target_properties(${target_name} PROPERTIES
INSTALL_COMPONENT ${INSTALL_COMPONENT}
)
endif()
endfunction()
# 使用示例
install_target(myapp
DESTINATION /usr/local
COMPONENT runtime
HEADERS include/myapp.h include/myapp_utils.h
FILES README.md LICENSE
)
这个函数封装了常见的安装逻辑,支持头文件、额外文件和组件安装。我在多个项目中复用这个函数,省了不少事。
6.7 知识体系图
6.8 总结几条实用建议
- 优先用function():除非你明确需要修改调用者变量,否则函数更安全。
- 参数多就用关键字:cmake_parse_arguments值得花10分钟学会。
- 返回值用PARENT_SCOPE:这是CMake函数返回值的标准做法。
- 宏里小心变量污染:给内部变量加前缀,避免和调用者冲突。
- 函数名加项目前缀:比如myproject_add_library,避免和其他模块冲突。
函数和宏是CMake模块化的基础。掌握了它们,你的CMakeLists.txt就能从"脚本"进化成"工程"了。
公众号:蓝海资料掘金营,微信deep3321