华南虎视觉组主页华南虎视觉组主页
首页
项目
通用
教程
Gitea
首页
项目
通用
教程
Gitea
  • 现代 CMake 教程

    • 【01】安装与基本介绍
    • 【02】CMake 的文件分布
    • 【03】变量的设置与引用
    • 【04】运算符与条件、循环语句
    • 【05】目标构建
    • 【06】变量参与 C++ 程序的编译
    • 【07】宏与函数
      • 1. 前言
      • 2. 宏、函数
        • 2.1 函数基本用法
        • 2.2 宏的基本用法
        • 2.3 函数与宏的区别
      • 3. 参数解析
        • 3.1 用法
        • 3.2 案例
    • 【08】find_package 详解
    • 【09】生成器表达式
    • 【10】项目的导出与安装
    • 【11】项目实战
    • 【12】CTest
    • 【13】CPack

【07】宏与函数

1. 前言

我们假设有以下文件架构:

.
├── core1
│   ├── CMakeLists.txt
│   ├── include
│   │   └── core1.hpp
│   └── src
│       ├── core1.cpp
│       ├── core_cal.cpp
│       └── util.cpp
└── core2
    ├── CMakeLists.txt
    ├── include
    │   └── core2.hpp
    └── src
        ├── core2.cpp
        ├── core_cal.cpp
        └── util.cpp

两个模块,同样的架构,因此需要同样的构建方式,假设core1依赖 OpenCV,假设core2依赖 core1:

# core1/CMakeLists.txt 部分内容
find_package(OpenCV REQUIRED)

aux_source_directory(src core1_dir)
add_library(
  core1
  SHARED ${core1_dir}
)
target_include_directories(
  core1
  PUBLIC include
)
target_link_libraries(
  core1
  PUBLIC ${OpenCV_LIBS}
)

# core2/CMakeLists.txt 部分内容
aux_source_directory(src core2_dir)
add_library(
  core2
  SHARED ${core2_dir}
)
target_include_directories(
  core2
  PUBLIC include
)
target_link_libraries(
  core2
  PUBLIC core1
)

可以看出,上面一段CMakeLists.txt的代码重复率比较高,每次创建库目标都是几乎一样的操作,为此我们可以使用宏、函数操作。这样可以使得构建操作缩短至几行,例如 OpenCV 的 core 模块:

ocv_add_module(core
               OPTIONAL opencv_cudev
               WRAP java objc python js)

其中,ocv_add_module就是 OpenCV 定义于OpenCVModule.cmake文件中的宏。

对于上述例子我们先给出结果,如果我们创建了这样一个宏

macro(add_module name)
  # 参数解析
  set(multy_args DEP_LIBS)
  cmake_parse_arguments(MOD "" "" "${multy_args}" ${ARGN})
  # 创建 target
  aux_source_directory(src _src_dir)
  add_library(${name} SHARED ${_src_dir})
  # 设置 target 属性
  target_include_directories(${name} PUBLIC include)
  target_link_libraries(${name} PUBLIC ${MOD_DEP_LIBS})
  unset(_src_dir)
endmacro()

我们就可以改写成以下形式

# core1/CMakeLists.txt 部分内容
find_package(OpenCV REQUIRED)

add_module(
  core1
  DEP_LIBS ${OpenCV_LIBS}
)
# core2/CMakeLists.txt 部分内容
add_module(
  core2
  DEP_LIBS core1
)

这种情况不仅减少了书写难度,还提高了可读性。

2. 宏、函数

2.1 函数基本用法

2.1.1 定义

function(my_func)
  # function
endfunction()

2.1.2 调用

函数调用的函数名不区分大小写,不过通常在 CMake 中函数名建议写小写。

my_func() # 可以
MY_FUNC() # 可以
mY_fUnC() # 有些逆天,但也可以

除此之外,一个函数还打开一个新的作用域,这点与C/C++一致。同样,使用set(xxx PARENT_SCOPE)可以提升变量作用域。

CMake 3.18 起,也可以使用cmake_language(CALL ...)子命令来调用函数:

function(my_foo x1 x2)
  message(STATUS "x1 = ${x1}, x2 = ${x2}")
endfunction()
cmake_language(CALL my_foo 2 7)

2.1.3 参数

使用形参

当函数被调用时,首先通过实参替换形参 ,然后作为普通命令调用。

function(my_foo x1 x2 x3)
  message(STATUS "x1 = ${x1}, x2 = ${x2}, x3 = ${x3}")
endfunction()
my_foo(2 7 4)
# 显示结果
# -- x1 = 2, x2 = 7, x3 = 4

使用 ARGC、ARGVn

除了引用形参之外,还可以引用ARGC变量 和ARGVn变量来引用参数,ARGC表示参数数量,以及ARGV0, ARGV1, ARGV2, ...将具有传入参数的值,有助于创建带有可选参数的函数。同时ARGV保存了所有的参数列表

function(my_foo)
  message(STATUS "ARGC = ${ARGC}")
  message(STATUS "x1 = ${ARGV0}, x2 = ${ARGV1}, x3 = ${ARGV2}")
endfunction()
my_foo(2 7 4)
# 显示结果
# -- ARGC = 3
# -- x1 = 2, x2 = 7, x3 = 4

使用 ARGN

此外,ARGN变量保存超过形参列表之后的参数。如果实参数量大于形参数量,用ARGN变量引用预期之外的参数。

function(my_foo x1)
  message(STATUS "x1 = ${x1}")
  message(STATUS "ARGN = ${ARGN}")
endfunction()
# 调用,执行了 set(x1 2)... 的操作
my_foo(2 7 4)
# 显示结果
# -- x1 = 2
# -- ARGN = 7;4

形参为变量

以上的例子中,参数都为具体的值,例如2, 7, 4,那么变量取值的语法${}作用于参数,获取到的就是具体的值。如果参数为变量的话,那么${}获取到的参数内容就是变量这个字符串,要想获取值的话就要使用 ${${}}。

set(a1 10)
function(foo val)
  message(STATUS "val, ${val}, ${${val}}")
endfunction()
foo(a1)
# 显示结果
# -- val, a1, 10

2.2 宏的基本用法

宏与函数最大的区别就是,宏不会创建新的作用域,而是单纯的发生了文本替换,这与C/C++完全一致

2.2.1 定义

macro(myFun)
  # macro
endmacro()

2.2.2 调用

  • 与函数一样,宏在调用时也可以不区分大小写,但同样建议使用小写。

  • CMake 3.18 起,也可以使用cmake_language(CALL ...)子命令来调用宏:

    macro(my_foo x1 x2)
      message(STATUS "x1 = ${x1}, x2 = ${x2}")
    endmacro()
    cmake_language(CALL my_foo 2 7)
    

2.2.3 参数

与函数一样,同样可以设置参数,也同时具备ARGC、ARGV、ARGVx、ARGN的参数,如果这些参数在宏内部,调用该宏的时候,这些内容会发生替换,例如

macro(my_macro xx)
  message(STATUS "xx   = ${xx}")   # 这里的 ${xx} 会被直接替换为 aa
  message(STATUS "ARGC = ${ARGC}") # 这里的 ${ARGC} 会被直接替换为 3
  message(STATUS "ARGV = ${ARGV}") # 这里的 ${ARGV} 会被直接替换为 aa;bb;cc
endmacro()

my_macro(aa bb cc)

# 打印结果
# -- xx   = aa
# -- ARGC = 3
# -- ARGV = aa;bb;cc

2.3 函数与宏的区别

  1. 宏不会开辟新的作用域,因此在宏内部定义的变量,将在外部继续生效

    macro(test)
      set(val "666")
    endmacro()
    
    set(val "123")
    message("val = ${val}")
    test()
    message("val = ${val}")
    
    # 打印结果
    # -- val = 123
    # -- val = 666
    

    这一点在函数就不会出现,内部设置的变量一般无法覆盖外部的变量,但如果需要覆盖,可以使用 PARENT_SCOPE 提升作用域。

  2. 宏在被调用的范围内有一个同名的变量,将使用现有变量而不是参数。

    macro(aaa)
      # 这里的 ${ARGN} 在调用时会发生替换,替换为 x;y;z
      foreach(m ${ARGN})
        message(STATUS "m = ${m}")
      endforeach() 
      message(STATUS "================")
      # 这里的 ARGN 没有引用,在调用时不会发生替换
      foreach(m IN LISTS ARGN)
        message(STATUS "m = ${m}")
      endforeach()  
    endmacro()
    
    function(bbb)
      aaa(x y z)
    endfunction()
    
    bbb(a b c)
    
    # 打印结果
    # -- m = x
    # -- m = y
    # -- m = z
    # -- ================
    # -- m = a
    # -- m = b
    # -- m = c
    

    这里的 ARGN 其实指代的是函数中的 ARGN

3. 参数解析

经常见到以下命令:

# 案例 1
target_include_directories(
  MyLib
  PUBLIC include
  PRIVATE _deps
)
# 案例 2
execute_process(
  COMMAND ls ${current_dir}
  OUTPUT_VARIABLE subs
)

这些内置的函数和宏可以对以上的include、_deps和ls ${current_dir}等内容进行解析,如果我们自己写的函数或宏,要如何实现这种参数解析的功能?这里要介绍一个 CMake 内置的命令:cmake_parse_arguments()

3.1 用法

有两个具体的用法:

cmake_parse_arguments(<prefix> <options> <one_value_keywords>
                      <multi_value_keywords> <args>...)

cmake_parse_arguments(PARSE_ARGV <N> <prefix> <options>
                      <one_value_keywords> <multi_value_keywords>)

通常使用第一个用法,第二个用法整体与第一个类似,有兴趣者可以参考CMake官网。下面主要对第一个用法做讲解。

  1. <options>:表示可选关键词列表,如果传入参数包含此变量名,则为TRUE,反之为 FALSE。

    例如,我们常见的INTERFACE,PUBLIC,PRIVATE都是target_include_directories函数中可选关键字列表中的元素。

    set(options INTERFACE PUBLIC PRIVATE)
    cmake_parse_arguments(xxx "${options}" "" "" ${ARGN})
    
  2. <one_value_keywords>:表示单值关键词列表,每个关键词仅对应一个值。

  3. <multi_value_keywords>:表示多值关键词列表,每个关键词可对应多个值。

    提示

    1. 实际上,能用<one_value_keywords>的都能用<multi_value_keywords>。
    2. 要解析的参数<args>...,我们一般传入为${ARGN}即可,一般定义的函数或宏是无参的,除非第一个参数不是关键词,那么有多少非关键词变量,定义多少形参。

    举个例子

    function(my_function)
      set(multi_args YYY ZZZ)
      cmake_parse_arguments("ABC" "" "" "${multi_args}" ${ARGN})
      message(STATUS "YYY: ${ABC_YYY}")
      message(STATUS "ZZZ: ${ABC_ZZZ}")
    endfunction()
    
    my_function(
      YYY 123 456 789
      ZZZ abc
    )
    
    # 打印结果
    # -- YYY: 123;456;789
    # -- ZZZ: abc
    
  4. prefix:我们将参数${ARGN}根据<options>,<one_value_keywords>,<multi_value_keywords>规则进行解析,解析出来的新变量名根据<prefix>前缀,按照 prefix_参数名的形式进行设置,例如:

    set(options INTERFACE PUBLIC PRIVATE)
    set(multy_args DEPENDS)
    cmake_parse_arguments(ABC "${options}" "" "${multy_args}" ${ARGN})
    # 访问 options 的内容是否为 ON
    if(${ABC_INTERFACE})   # ABC_ 前缀
    elseif(${ABC_PUBLIC})  # ABC_ 前缀
    elseif(${ABC_PRIVATE}) # ABC_ 前缀
    endif()
    # 访问 multy_args 的内容
    message(STATUS "${ABC_DEPENDS}") # ABC_ 前缀
    

3.2 案例

最后,来看 RMVL 中使用有关添加测试用例的宏来做巩固:

# 在当前目录中添加新的测试用例
# 用法:
# rmvl_add_test(
#   <name>
#   [DEPENDS <rmvl_target>]
#   [DEPEND_TESTS <test_target>]
# )
# 示例:
# rmvl_add_test(
#   detector                       # 测试名
#   DEPENDS armor_detector         # 需要依赖的 RMVL 目标库
#   DEPEND_TESTS GTest::gtest_main # 需要依赖的第三方测试工具目标库
# )
macro(rmvl_add_test _name)
  # add arguments variable    
  set(multy_args DEPENDS DEPEND_TESTS)
  cmake_parse_arguments(TEST "" "" "${multy_args}" ${ARGN})
  # add testing executable
  set(test_dir)
  aux_source_directory(test test_dir)
  add_executable(rmvl_${_name}_test ${test_dir})
  # depends
  foreach(_dep ${TEST_DEPENDS})
    target_link_libraries(
      rmvl_${_name}_test
      PRIVATE rmvl_${_dep}
    )
  endforeach(_dep ${TEST_DEPENDS})
  # test depends
  target_link_libraries(
    rmvl_${_name}_test
    PRIVATE ${TEST_DEPEND_TESTS}
  )
  gtest_discover_tests(rmvl_${_name}_test)
endmacro(rmvl_add_test _name)
Prev
【06】变量参与 C++ 程序的编译
Next
【08】find_package 详解