25、函数与宏:function()和macro()的定义与调用,参数传递

CMake 里写重复逻辑,最直接的办法就是封装。我见过不少项目,CMakeLists.txt 动辄上千行,里面全是重复的 add_librarytarget_link_libraries,看着就头疼。其实用 function()macro() 就能轻松搞定。

这一讲,咱们就聊聊 CMake 里的函数和宏。说白了,它们就是让你把一段逻辑包起来,起个名字,然后到处调用。但两者有本质区别,用错了会踩坑。

25.1 为什么需要函数和宏?

你想想看,一个项目里可能有十几个子模块,每个模块都要设置编译选项、链接库、安装路径。如果每次都手写一遍,不仅累,还容易漏掉某个参数。封装成函数或宏,一次定义,到处复用。

我在项目中遇到过最典型的场景:团队里每个人写的 CMake 风格都不一样,有人用 set 直接改全局变量,有人到处写 add_definitions。后来我统一封装了几个函数,大家照着用,代码立马清爽了。

25.2 function() 的定义与调用

function() 会创建一个新的作用域。什么意思呢?函数内部定义的变量,默认不会影响外部。这跟 C++ 里的函数很像——局部变量只在函数内有效。

先看个最简单的例子:

function(my_print message)
    message(STATUS "【自定义打印】${message}")
endfunction()

my_print("Hello, CMake!")

调用的时候,直接写函数名加参数就行。注意,参数之间用空格分隔,不需要逗号。

函数内部可以通过 ${ARGV} 拿到所有参数,${ARGC} 拿到参数个数。我习惯用 ${ARGV} 来处理不定参数,灵活很多。

关键点:function 内部用 set(var value PARENT_SCOPE) 才能把变量传出去。否则外部看不到。

25.3 macro() 的定义与调用

macro() 就不一样了。它不会创建新作用域,本质上就是文本替换。宏内部的所有变量操作,直接作用在调用者的作用域里。

看个对比:

macro(my_macro message)
    set(result "宏处理: ${message}")
endmacro()

my_macro("测试")
message(STATUS "result = ${result}")  # 能打印出来

换成 function 试试:

function(my_func message)
    set(result "函数处理: ${message}")
endfunction()

my_func("测试")
message(STATUS "result = ${result}")  # 打印为空!

为什么会这样?因为函数里的 set 默认只在函数内部生效。想传出来?加 PARENT_SCOPE

注意:宏里的 return() 行为跟函数不同。宏里的 return() 会直接跳出整个调用链,容易引发意外。我曾经在宏里用 return() 想提前结束,结果把外层的 foreach 也给跳了……

25.4 参数传递的细节

CMake 的参数传递,有几个点容易搞混。我列个表,一目了然:

变量 含义 示例
${ARGC} 参数个数 3
${ARGV} 所有参数列表 a;b;c
${ARGN} 超出命名参数的部分 如果函数定义了两个命名参数,ARGN 就是第三个及以后
${ARGV0} ~ ${ARGV<N>} 按索引访问单个参数 ${ARGV0} 是第一个参数

我个人习惯用 ${ARGN} 来处理可选参数。比如:

function(my_target name)
    # 第一个参数是 name,剩下的都当作源文件
    set(sources ${ARGN})
    add_library(${name} ${sources})
endfunction()

my_target(my_lib a.cpp b.cpp c.cpp)

这样调用起来很自然,不需要把所有源文件都塞到一个列表变量里。

25.5 函数 vs 宏:怎么选?

我个人的经验是:优先用 function。原因很简单——作用域隔离,不容易出幺蛾子。宏虽然写起来省事,但副作用太隐蔽。

什么时候用宏?

  • 你需要修改调用者作用域里的变量
  • 你封装的是非常简单的文本替换逻辑
  • 你明确知道宏的副作用,并且能控制住

举个例子,我有时候用宏来封装一些调试开关:

macro(enable_debug)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DDEBUG -g")
endmacro()

这种场景用宏就很合适——我就是要修改外部的 CMAKE_CXX_FLAGS

小技巧:如果你不确定用哪个,先写 function。发现需要往外传变量时,再改成 macro 或者用 PARENT_SCOPE。别一开始就上宏,容易失控。

25.6 实战:封装一个通用库加载函数

咱们来写个实际能用的。假设项目里有很多第三方库,每个库都要找头文件、链接库、设置宏定义。封装一下:

function(load_third_party name)
    # 参数:name 库名,后面跟可选的 include_dirs lib_dirs
    set(one_value_args INCLUDE_DIRS LIB_DIRS)
    set(multi_value_args DEFINES)
    cmake_parse_arguments(PARSE_ARGV 1 "${name}" "" "${one_value_args}" "${multi_value_args}")

    # 找头文件
    if(${name}_INCLUDE_DIRS)
        include_directories(${${name}_INCLUDE_DIRS})
    endif()

    # 找库文件
    if(${name}_LIB_DIRS)
        link_directories(${${name}_LIB_DIRS})
    endif()

    # 添加宏定义
    foreach(def ${${name}_DEFINES})
        add_definitions(-D${def})
    endforeach()

    message(STATUS "已加载第三方库: ${name}")
endfunction()

# 调用示例
load_third_party(OpenCV
    INCLUDE_DIRS /usr/local/include/opencv4
    LIB_DIRS /usr/local/lib
    DEFINES USE_OPENCV OPENCV_VERSION_4
)

这里用了 cmake_parse_arguments,这是 CMake 官方推荐的参数解析方式。支持单值参数和多值参数,调用起来跟系统命令一样清晰。

25.7 避坑指南

最后分享几个我踩过的坑:

  • 宏里的 return 会跳出外层:我曾经在宏里写 return() 想提前结束,结果把外层的 function 也给跳了。排查了半天……
  • 函数里忘记 PARENT_SCOPE:在函数里 set 了一个变量,外面死活拿不到。后来才发现默认只在局部生效。
  • 参数名跟变量名冲突:比如函数参数叫 name,内部又用 set(name xxx),会把参数值覆盖掉。建议参数名加个前缀,比如 _name
  • ARGV 在宏里的行为:宏里 ${ARGV} 是调用者的参数,不是宏自己的。这个跟函数不一样,容易混淆。

嗯,函数和宏就聊这么多。记住核心区别:函数有作用域,宏没有。选对工具,你的 CMake 代码会干净很多。

CMake 函数与宏核心知识图 function() vs macro() function() macro() 特性 • 创建独立作用域 • 变量默认不对外影响 • 用 PARENT_SCOPE 传出变量 特性 • 文本替换,无作用域 • 变量直接修改调用者 • return() 会跳出外层 参数传递 • ARGC / ARGV / ARGN • cmake_parse_arguments 解析 • 推荐优先用 function()
公众号:蓝海资料掘金营,微信deep3321