CMake实践

CMake 入门3 —— CMake 的函数和宏

2020-11-28  本文已影响0人  你可记得叫安可

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}) 时,其实展开后的宏定义代码块变为了:

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’

2. 原代码的变形

如果我们将上面宏定义中的 set 方法修改为 set(${arg} "abc") 呢?
其实同样不会改变外层的 var 值。因为如我们上面所说,宏定义只做一件事:将所有的 ${参数} 的地方都以字符串替换的方式,替换为传进来的值。因此,如上修改后只会变为 set(ABC abc),再次定义了一个变量 ABC,且赋值为字符传 ‘abc’

关于 function 的事实

CMake 中的 function 就更像我们传统意义上的函数了。我们有两种方式调用函数:

阶段总结

如何传递列表类型的参数?

如果我要打印一个列表要怎么写?

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}") 时的输出

2. 当使用 print_list(${arg}) 时的输出:

函数的应用:递归搜索所有目录

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})
上一篇 下一篇

猜你喜欢

热点阅读