CMake 中 function 和 macro 的区别
通过代码直观地来看看区别。
set(var "ABC")
macro(Moo arg)
message("arg = ${arg}") # 输出原值 ABC
set(arg "abc")
message("# After change the value of arg.")
message("arg = ${arg}") # 输出原值 ABC
endmacro()
message("=== Call macro ===")
Moo(${var})
function(Foo arg)
message("arg = ${arg}") # 输出原值 ABC
set(arg "abc")
message("# After change the value of arg.") # 输出修改后的值 abc
message("arg = ${arg}")
endfunction()
message("=== Call function ===")
Foo(${var})
上面的例子来自 function-vs-macro-in-cmake。其中最佳答案的评论中还提到了一个非常有用的 cmake
参数 --trace-expand
,该指令会将 cmake
中的宏定义展开,这样就方便了我们仔细研究 cmake
中的宏定义。
通过执行 cmake .. --trace-expand
我们可以知道以下事实:
关于 macro 宏定义的事实
1. 原代码的解析
- 在上面的例子中,宏定义
Moo
接受的参数其实是var
的值,字符串"ABC"
。 - 在宏定义体中,需要引用参数的话,只能写
${参数}
。宏定义会将所有${参数}
的地方进行直接简单粗暴地替换为字符串"ABC"
。
因此上面的代码中,当我们调用 Moo(${var})
时,其实展开后的宏定义代码块变为了:
Moo(ABC)
message(arg = ABC)
set(arg abc)
message(# After change the value of arg.)
message(arg = ABC)
因此,宏定义中的 set
方法试图改变宏定义中传进来的参数 arg
是不可能的。set(arg "abc")
只是定义了一个变量 arg
,且赋值为字符串 ‘abc’
。
在 CMake 中,只要是不带空格的值,其本质上都是字符串。因此上面的
message(arg = ABC)
同message(arg = "ABC")
是一样的。
2. 原代码的变形
如果我们将上面宏定义中的 set
方法修改为 set(${arg} "abc")
呢?
其实同样不会改变外层的 var
值。因为如我们上面所说,宏定义只做一件事:将所有的 ${参数}
的地方都以字符串替换的方式,替换为传进来的值。因此,如上修改后只会变为 set(ABC abc)
,再次定义了一个变量 ABC
,且赋值为字符串 'abc’
。
关于 function 的事实
CMake
中的 function
就更像我们传统意义上的函数了。我们有两种方式调用函数:
-
Foo(${var})
。当我们通过这样的方式去调用函数时,CMake 会将变量进行展开,依次匹配函数声明中的参数列表。同时,它也相当于我们传统函数调用中的值传递,也就是说函数中对参数的修改,并不会影响到调用时传进来的变量var
。 -
Foo(var)
。这种调用其实应该是错误的(不符合我们的目的预期),但是由于CMake
没有那么严格的类型要求,因此这种写法是可以通过编译的。它的本质上是将字符串'var'
赋值给了Foo
的局部变量(也就是参数)arg
。因此第一条message
会输出 arg = var。而第二条命令set(arg "abc")
会将局部变量arg
的值赋值为字符串'abc'
,因此最后一条message
会输出 arg = abc。
阶段总结
- 当我们使用
macro
宏定义时,一定要清楚地明白宏只做一件事情:替换宏定义代码块中${参数}
的部分为 调用时传进来的值。 - 当我们使用
function
时,一定要使用Foo(${var})
这样的调用。只有这样,方法体中的函数参数值才会是传入进来的变量值。同传统函数的值传递一样,方法体中对参数的所有访问和修改,都不会影响到外面调用时传进来的变量。
如何传递列表类型的参数?
如果我要打印一个列表要怎么写?
set(arg hello wolrd)
foreach(v ${arg})
message(${v})
endforeach()
输出:
hello
world
在调试 CMake 脚本的时候,经常会用到这种打印列表的代码,于是很自然地我们需要一个打印列表的函数:print_list
:
function(print_list arg)
foreach(v ${arg})
message(${v})
endforeach()
endfunction()
然后我们如下使用这个函数:
set(arg hello wolrd)
print_list(${arg})
这时我们会发现输出只有一个 hello。我们的预期是输出 hello wolrd,但是却只有一个 hello。
这个问题其实是出在对函数 print_list
的调用方式上:print_list(${arg})
展开来看就是 print_list(hello world)
,因此,传递给 print_list
的第一个参数只有 hello。
正确的调用方式应该是下面这样,使用双引号把参数括起来:
print_list("${arg}")
函数里的隐含变量
会出现上一节中的问题,主要是因为没有明白,如果展开后的参数个数多于函数声明时的参数个数,那么函数将会区分已声明的参数(对应函数参数列表里有名字的,我自己称它为有名参数)和 未声明的额外参数(对应函数参数列表里没有找到名字的,我自己称它为无名参数)。CMake 中其实包含了有名参数、无名参数相关的一些隐含变量。
name | description |
---|---|
ARGC | 函数所有实参的个数,包括有名参数、无名参数 |
ARGV | 所有实参列表,包括有名参数、无名参数 |
ARGN | 所有的额外实参,即无名参数 |
ARGV0 | 第 1 个实参 |
ARGV1 | 第 2 个实参 |
ARGV2 | 第 3 个实参 |
ARGVn | 第 n 个实参 |
使用上面表格里的几个隐含变量,我们就可以知道上一节中的两种函数传递参数的方式,函数内部发生了什么:
function(print_list arguments)
message("=== arguments: ${arguments} ===") # 打印参数 arguments
message("=== args count: ${ARGC} ===") # 所有参数的个数
message("=== all args ===")
foreach(v IN LISTS ARGV)
message(${v})
endforeach()
message("=== all extra args ===") # 打印所有额外参数
foreach(v IN LISTS ARGN)
message(${v})
endforeach()
message("=== print content of ARG0 ===") # 打印第 1 个参数
foreach(v IN LISTS ARGV0)
message(${v})
endforeach()
message("=== print content of ARG1 ===") # 打印第 2 个参数
foreach(v IN LISTS ARGV1)
message(${v})
endforeach()
endfunction()
set(arg hello wolrd)
message("--- arg: ${arg} ---") # 先打印下原始的 arg 参数
message("--- calling with quotes ===") # 使用引号来调用
print_list("${arg}")
message("--- calling without quotes ---") # 不使用引号调用
print_list(${arg})
输出如下:
--- arg: hello;world ---
--- calling with quotes ===
=== argument: hello;wolrd ===
=== args count: 1 ===
=== all args ===
hello
wolrd
=== all extra args ===
=== print content of ARG0 ===
hello
wolrd
=== print content of ARG1 ===
--- calling without quotes ---
=== argument: hello ===
=== args count: 2 ===
=== all args ===
hello
wolrd
=== all extra args ===
wolrd
=== print content of ARG0 ===
hello
=== print content of ARG1 ===
wolrd
从输出中其实我们就可以看到,在调用函数之前,我们先打印了变量 arg
中的内容,输出是 --- arg: hello;wolrd ---。这里打印出来时,两个值使用分号连接,这是 CMake 中列表类型的表示方式。说明原参数 arg
是一个列表类型。
事实上,对于参数
arg
的赋值,还可以写成:set(arg hello; wolrd)
,这样能更加显式地表明arg
是一个列表类型。
1. 当使用 print_list("${arg}")
时的输出:
-
=== argument: hello;wolrd ===:使用引号包裹参数时,
"${arg}"
将整体作为一个列表类型传入到函数print_list
的第一个参数arguments
中。 -
=== args count: 1 ===:这里很好理解,由于
"${arg}"
作为一个列表类型整体传入了函数,因此参数个数为 1。 - 理解了上面两点,那么后面的打印内容都很好理解了。
2. 当使用 print_list(${arg})
时的输出:
-
=== argument: hello ===:不使用引号包裹参数时,传入的列表类型参数
${arg}
将被展开,依次匹配函数声明的参数列表。由于print_list
只声明了一个参数arguments
,因此arguments
被赋值为hello
。另一个world
则成为了匿名参数,需要通过ARGV1
来访问。 - === args count: 2 ===:参数个数为两个,包括了有名参数 和 无名参数。
- 理解了上面两点,那么后面的打印内容都很好理解了。
函数的应用:递归搜索所有目录
CMake 中有个命令是带有递归含义的:file(GLOB_RECURSE cpp_list ./*.cpp)
。
这个 file
命令使用 GLOB_RECURESE
参数的时候即表示递归搜索的意思,上面这句话的意思就是递归搜索当前目录及其子目录下的所有 .cpp
文件,把其完整路径放入列表 cpp_list
中。
通常情况下,确定了所有原文件的路径,对于一个工程的构建来说就已经完成了一大半,剩下的问题就是库和头文件的搜索路径。
库的搜索路径通常都很简单,因为通常不需要连接很多的库,并且库可以统一存放。
最后的问题就是头文件的搜索路径问题,在一个组织良好的项目里,公用的头文件通常放在一个公共的 include
路径下,业务逻辑里的头文件通常和其源文件放在相同的路径下,此时在其源文件中使用 #include
时候,即使没有写完整的包含路径,仅仅写 #include "header.h"
也能够编译通过。然而在代码组织非常差的工程中,最坏情况下,我们可能需要搜索所有的目录。
所以,我们需要一个函数,递归的搜索指定目录的子目录,把所有的子目录添加到 include
路径里。
function(include_sub_directories_recursively root_dir)
if(IS_DIRECTORY ${root_dir}) # 当前路径是一个目录吗,是的话就加入到包含目录
message("include dir: " ${root_dir})
include_directories(${root_dir})
endif()
file(GLOB all_sub RELATIVE ${root_dir} ${root_dir}/*) # 获得当前目录下的所有文件,存入 all_sub 列表中
message("all sub ${all_sub}")
foreach(sub ${all_sub})
if(IS_DIRECTORY ${root_dir}/${sub})
include_sub_directories_recursively(${root_dir}/${sub}) # 对子目录递归调用
endif()
endforeach()
endfunction()
include_sub_directories_recursively(${CMAKE_CURRENT_LIST_DIR})