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 知识体系图

CMake 函数与宏知识体系 function() / macro() 定义方式 function(name ...) / macro(name ...) endfunction() / endmacro() 参数传递 位置参数:${ARGC} ${ARGV} 关键字参数:cmake_parse_arguments 可变参数:${ARGN} 返回值处理 PARENT_SCOPE 变量传递 输出参数模式 return() 提前退出 作用域差异 function: 独立作用域 macro: 调用者作用域

6.8 总结几条实用建议

  • 优先用function():除非你明确需要修改调用者变量,否则函数更安全。
  • 参数多就用关键字:cmake_parse_arguments值得花10分钟学会。
  • 返回值用PARENT_SCOPE:这是CMake函数返回值的标准做法。
  • 宏里小心变量污染:给内部变量加前缀,避免和调用者冲突。
  • 函数名加项目前缀:比如myproject_add_library,避免和其他模块冲突。

函数和宏是CMake模块化的基础。掌握了它们,你的CMakeLists.txt就能从"脚本"进化成"工程"了。


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