Cpp构建和编译笔记——7.CMake语法结构
在上一篇已经学习了作为一门语言的最基本内容,接下来是进一步的内容
- 流程控制(条件语句,循环语句)
- 函数和宏
- cmake 脚本/模块(不是 CMakeLists,而是.cmake 文件)
条件判断
if 语句
最完整的 if 语法结构如下
1 | if(<condition>) |
其中的 elseif 和 else 都是可选的,例如
1 | if(WIN32) |
正如前文中提到的,在 if
后面的变量,不需要使用${Var}
的形式获取 Var
的值,而是直接使用 Var。
条件语法
在 if 中条件,也就是if(P)
中的命题 P
可以实现丰富的功能,更详细的讨论可以参考CMake官方文档
因为 if 语句出现的太早了,导致if(P)
的语法看起来非常奇怪:
尝试对一个变量名称自动求值,if(${P})
。
如果希望处理一个可能是变量名的字符串,建议使用双引号if("${P}")
,这会抑制
if
的自动求值。总之要么用if(P)
要么用if("${P}")
基本变量
P 可以是最基本的常量,字符串或者变量名。
if(<constant>)
: P 是有意义的常量1, ON, YES, TRUE, Y
\(\Rightarrow\) True- 非零的数,甚至浮点数 \(\Rightarrow\) True
0, OFF, NO, FALSE, N, IGNORE, NOTFOUND, *-NOTFOUND
\(\Rightarrow\) False- 空字符串 \(\Rightarrow\) False
- 这里的布尔变量不区分大小写,例如 True,true,TRUE 等都是可以的
- 其它情形会被视作一个变量或一个字符串进行处理
if(<variable>)
: P 是一个变量的名称(而非变量的值)- 变量已定义,并且变量的值不是上述 False 常量的情形 \(\Rightarrow\) True
- 变量已定义,但是变量的值是上述 False 常量的情形 \(\Rightarrow\) False
- 变量未定义 \(\Rightarrow\) False
- 上述规则对宏结构不使用,对环境变量也不使用(环境变量的名称总是得到 False)
if(<string>)
: P 是字符串- 可以被解析为 True 常量的字符串 \(\Rightarrow\) True
- 通常情形下,其它的字符串 \(\Rightarrow\) False
常见的例如
1 | set(A "") # 空字符串 |
逻辑判断
P 可以是一些简单的逻辑判断
1 | # 取反运算 |
可以支持基于与或非的复杂组合,例如
1 | if((condition1) AND (condition2 OR (condition3))) |
注意:
- 可以使用小括号改变计算优先级
- NOT 的优先级比 AND/OR 更高
- 对于 AND、OR
计算顺序从左到右,并且不使用任何短路操作:
x AND y
在 x 不正确时仍然解析 y
存在性判断
if(COMMAND command-name)
: 判断这个 command-name 是否属于命令、可调用的宏或者函数的名称,则返回 Trueif(TARGET target-name)
: 判断这个 target 是否已经被add_executable(), add_library(), add_custom_target()
这类命令创建,即使 target 不在当前目录下if(DEFINED <name>|CACHE{<name>}|ENV{<name>})
: 判断这个变量是否已定义if(<variable|string> IN_LIST <variable>)
: 判断这个变量或字符串是否在列表中,见下文的列表操作
常见的比较
- 数字的比较
1 | # 小于 |
- 字符串的比较(字典序)
1 | if(<variable|string> STRLESS <variable|string>) |
- 版本号比较(版本号格式为
major[.minor[.patch[.tweak]]]
,省略的部分被视为零)
1 | if(<variable|string> VERSION_LESS <variable|string>) |
路径与文件判断
比较简略,因为目录和文件需要考虑平台和格式等,细节比较多,用到再查文档吧
1 | # 完整路径是否存在,这里~开头的还不行 |
foreach 循环
基本用法
foreach
循环最基本的用法就是遍历一个列表中的所有变量,此时需要使用${Var}
先解析列表的值,例如
1 | set(A 1;2;3;4) |
由于我们已经把列表解析了,所以下面的用法也是一样的
1 | foreach(x a b c) |
更本质地说,foreach 基本用法中,cmake 会把第一个位置的字符串定义为循环变量,把剩下的字符串数组视作迭代的列表。
等差数列遍历
foreach
支持基于等差数列的遍历,可以使用foreach(... RANGE ...)
命令,
其中range n
包括从 0 到 n
的自然数序列,range a b c
可以控制起点终点和步长,语法为
1 | foreach(<loop_var> RANGE <stop>) |
例如
1 | foreach(X RANGE 10) |
遍历多个列表
foreach
可以同时在多个列表中遍历,需要使用关键词foreach(... IN ZIP_LISTS ...)
,总的迭代次数以最长的列表为准,此时较短的列表对应取空值。
方式一,要求循环变量和列表的个数一致,此时每一个循环变量对应在一个列表中遍历,互不干扰
1 | foreach(<loop_vars> IN ZIP_LISTS <lists>) |
例如
1 | foreach(en ba IN ZIP_LISTS English Bahasa) |
方式二,通过单个变量loop_var
的多个分量loop_var_N
实现
1 | foreach(<loop_var> IN ZIP_LISTS <lists>) |
这里只用一个 loop_var,来承接对后面多个列表的遍历,它通过分量 loop_var_N 变量记录对应列表的当前项,例如
1 | list(APPEND English one two three four) |
其它
和其它语言一样,cmake
支持使用break()
命令提前终止循环,使用continue()
命令可用于立即开始下一次迭代。
cmake 还支持 while 循环,但是感觉用处不大,因为 cmake 不需要很复杂的循环逻辑,foreach 提供基本的遍历已经足够了。
1 | while(<condition>) |
例如
1 | set(i 0) |
函数
函数的语法结构如下:定义名为<name>
的函数,该函数接收名为<arg1>...
的参数,<commands>
表示函数定义的功能,在调用函数之前不会被执行。
1 | function(<name> [<arg1> ...]) |
可以使用<name>(...)
调用执行函数
- 函数体的内部存在一个独立的作用域,在内部定义和使用的变量默认都是"局部"的,不会影响到函数体外
- 调用函数时,对函数名称的大小写不敏感,
Foo()
,FOO()
,foo()
效果一样,建议使用全小写 - 函数的参数并没有形式和个数的要求,可以传入任意多的参数
- 函数会把传入的参数视作普通的字符串,把它作为指定名称的参数变量的值,而不是视作传入一个变量进行复制
- 函数执行时,会首先对函数体内部使用到的函数参数进行一轮替换,然后逐个执行命令
cmake 语法设计非常混乱,最基本的对于函数的参数都有很多种调用的方法,这些方法可以混合使用,不会互相冲突。
参数方式(一)
第一种方式,使用 cmake 默认提供的函数参数代词(注意,它们不是通常的变量,因为在调用函数时会直接全部替换掉)
- ARGC: 实际传入的参数的个数,与函数定义时预期的参数个数无关
- ARGV: 实际传入的参数全体(以列表的形式)
- ARGV#: 实际传入的第
#
个参数,计数从 0 开始的每一个参数,例如 ARGV0 为第一个参数,ARGV1 为第二个参数,依次类推(注意,对于计数超过ARGC
的ARGV#
,属于未定义的量) - ARGN: 预料之外的参数,定义宏(函数)时参数为 2 个,实际传了 4 个,则 ARGN 代表剩下的两个
例如,定义函数如下
1 | function(test) |
没有给出一个预期的参数,三组测试结果如下
1 | test(1 2 3) |
参数方式(二)
第二种方式,给 function 提供默认的形参名,此时传入的参数会依次分配给对应的形参,多的部分还是只能通过ARGN获取。
1 | function(test2 var) |
测试结果如下
1 | test2("abc") # var=abc |
这个例子说明:函数只会把解析后传入的形参作为纯粹的字符串常量,视作实参变量的值,并不是视作变量的重命名。如果坚持使用
test2(a),那么在 test2
内部,必须使用${${var}}
双层调用,来获取 a
的值。(不建议)
注意,${var}
不同于"${var}"
(其中的 var
是一个列表),在函数调用时因为解析的逻辑导致不同的结果
1 | function(PrintVar var) |
即列表作为参数时
列表 var 作为参数传入函数时:
调用时写成
${var}
,会被展开识别为多个参数调用时写成
"${var}"
,会被打包作为一个列表整体,分配给一个参数调用时写成
var
,在函数内需要两层解析${${var}}
才能得到值列表
1 | function(hello var) |
参数方式(三)
推荐在复杂参数情形下,使用cmake_parse_arguments
命令来解析函数或宏的参数,因为这个命令非常强大。
(不推荐与其他参数方式混用,不要在 function
命令中指定参数名称,而是放到函数体内部,使用cmake_parse_arguments
命令实现)
语法形式为
1 | cmake_parse_arguments(<prefix> <options> <one_value_keywords> |
我们通过例子来解释这个命令
1 | function(test) |
其中的"ARG"是我们指定的前缀,避免名称冲突,此时 cmake
会自动创建若干名称为ARG_XXX
的变量,来承接传入的参数。
- 调用函数时,如果传入了 OA 或者其它的 argop
中的内容,则定义
ARG_OA
变量为真
1 | test(OA ...) |
- 调用函数时,如果传入了 SA 并且附带一个值
value,则定义
ARG_SA
变量,它的值即为 value
1 | test(... SA "abc") |
- 调用函数时,如果传入了 LA 并且附带一个列表
values,则定义
ARG_LA
变量,它的值即为 values
1 | test(... LA "a;b;c") |
- 习惯在命令的最后使用
${ARGN}
,承接一些额外的参数
测试结果如下
1 | # 第一个,不使用任何参数 |
以上是正确使用三类参数的情形,可以发现我们并不需要按照选项、单值参数和列表参数的顺序,只需要在正确的关键字后面跟上值或者列表即可。
换言之,cmake_parse_arguments
命令使得我们可以用无序的参数键值对的形式,向调用的函数传入指定名称指定值的参数。
1 | cmake_parse_arguments(<prefix> <options> <one_value_keywords> |
cmake_parse_arguments
会解析传入的所有参数,按照指定的关键词和逻辑去分析捕获参数,并产生带有指定前缀的一组新变量:
<prefix>
是我们希望给捕获产生的所有新变量添加的前缀- 如果遇到
<options>
中的关键词,则对应产生的新变量为 true;否则默认为 false - 如果遇到
<one_value_keywords>
中的关键词,则把随后的一个值赋给产生的新变量;否则默认为空(未定义) - 如果遇到
<multi_value_keywords>
中的关键词,则把随后的一个列表赋给产生的新变量,直到遇见下一个关键词;否则默认为空(未定义) - 最后的
<args>...
部分,习惯上都使用${ARGN}
- 如果给的参数无法被 cmake
按照上述逻辑捕获,仍然可以通过传统的
ARGN
方式获得:由于我们没有在 function 命令指定任何参数名,其实所有的参数都被扔进了ARGN
或ARGV
。
变量作用域
在函数内部直接使用 set 命令不会影响到函数体之外,在退出函数体之后变量即失效
1 | function(foo) |
可以使用 PARENT_SCOPE,表明退出这一层作用域后,变量仍然存在
1 | function(foo) |
例如
1 | message("(1)A=${A}") |
这里体现的不仅仅是函数变量,而是所有的普通变量都具有的变量作用域机制:(与之相对的,缓存变量是全局的)
- 通过 add_subdirectory()进入新的 CMakeLists.txt 具有独立的变量作用域。
- 通过 include 导入的.cmake 文件没有独立的变量作用域。
- 自定义函数具有独立的变量作用域。
- 自定义的宏没有独立的变量作用域。
普通变量对子作用域可见,但是子作用域中的变量不会影响到父作用域,相当于定义了同名变量并覆盖,除非使用 PARENT_SCOPE 选项。
函数返回值
cmake
的函数可以在函数体的任何地方直接返回,使用return()
即可,但是
cmake 的函数语法有一个致命的问题,居然不支持返回值!在 CMake
中为了得到函数的返回值,我们必须把一个变量传给函数,然后通过提升作用域的方式实现,例如
1 | function(test_return rst arg) |
还可以这么干,直接使用一个变量负责传入传出,需要两层解析来获取传入的值,再使用PARENT_SCOPE
把变量传回去
1 | function(test_return2 rst) |
函数执行状态
在函数体内,可以使用如下的特殊变量,获取当前的函数执行状态
CMAKE_CURRENT_FUNCTION
当前执行的函数名CMAKE_CURRENT_FUNCTION_LIST_DIR
当前执行的函数所在 CMakeLists.txt 文件的目录CMAKE_CURRENT_FUNCTION_LIST_FILE
当前执行的函数所在 CMakeLists.txt 文件名(完整带路径)CMAKE_CURRENT_FUNCTION_LIST_LINE
当前执行的函数所在的行数(function 语句的行号)
宏
宏的标准语法如下,定义名为<name>
的宏,接收名为<arg1>...
的参数,<commands>
表示宏定义的功能,在调用宏之前不会被执行。
1 | macro(<name> [<arg1> ...]) |
宏和函数很相似,宏的名称是大小写不敏感的,建议完全小写。宏也支持使用ARGV
之类的默认参数名称,或者使用指定参数名称的方式,再或者cmake_parse_arguments
的方式。
我们重点关注宏和函数的不同点:
- 宏并没有像函数一样单独开辟一个作用域,而是简单地执行了两步操作
- 完成对参数的字符串替换
- 把命令部分拷贝过来执行
- 在宏的内部禁止使用
return()
命令,因为这个命令的效果不是退出宏,而是退出上一层 - 在宏的内部没有单独作用域,而且
ARGV0
不是通常的变量,只是类似于单纯的字符串替换,因此不能使用如下的语法
1 | if(ARGV0) # 在函数中可以,但是在宏内部错误,if不会解析ARGV0 |
模块
模块就是以 xxx.cmake
结尾的文件,通常将一些通用的函数或宏封装到到一个指定的文件中,然后通过include(xxx)
方式引用,可以达到代码复用的目的(但是并不会创建独立的变量作用域)。
模块既可以被 CMakeLists.txt 引用,也可以被其它模块引用。
模块的查找可以是从当前位置出发的相对路径,此时需要带文件后缀,例如include(cmake/xxx.cmake)
;也可以从CMAKE_MODULE_PATH
查找,此时只需要include(xxx)
,在查找前可以修改这个变量,让
CMake 顺利找到模块。