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

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})
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351

推荐阅读更多精彩内容