【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 函数与宏的区别
宏不会开辟新的作用域,因此在宏内部定义的变量,将在外部继续生效
macro(test) set(val "666") endmacro() set(val "123") message("val = ${val}") test() message("val = ${val}") # 打印结果 # -- val = 123 # -- val = 666
这一点在函数就不会出现,内部设置的变量一般无法覆盖外部的变量,但如果需要覆盖,可以使用
PARENT_SCOPE
提升作用域。宏在被调用的范围内有一个同名的变量,将使用现有变量而不是参数。
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官网。下面主要对第一个用法做讲解。
<options>
:表示可选关键词列表,如果传入参数包含此变量名,则为TRUE
,反之为FALSE
。例如,我们常见的
INTERFACE
,PUBLIC
,PRIVATE
都是target_include_directories
函数中可选关键字列表中的元素。set(options INTERFACE PUBLIC PRIVATE) cmake_parse_arguments(xxx "${options}" "" "" ${ARGN})
<one_value_keywords>
:表示单值关键词列表,每个关键词仅对应一个值。<multi_value_keywords>
:表示多值关键词列表,每个关键词可对应多个值。提示
- 实际上,能用
<one_value_keywords>
的都能用<multi_value_keywords>
。 - 要解析的参数
<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
- 实际上,能用
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)