在上一篇已经学习了作为一门语言的最基本内容,接下来是进一步的内容

  • 流程控制(条件语句,循环语句)
  • 函数和宏
  • cmake 脚本/模块(不是 CMakeLists,而是.cmake 文件)

条件判断

if 语句

最完整的 if 语法结构如下

1
2
3
4
5
6
7
if(<condition>)
<commands>
elseif(<condition>) # optional block, can be repeated
<commands>
else() # optional block
<commands>
endif()

其中的 elseif 和 else 都是可选的,例如

1
2
3
4
5
6
7
if(WIN32)
message(STATUS "Now is Windows")
elseif(APPLE)
message(STATUS "Now is Apple systens.")
elseif(UNIX)
message(STATUS "Now is UNIX-like OS's.")
endif()

正如前文中提到的,在 if 后面的变量,不需要使用${Var}的形式获取 Var 的值,而是直接使用 Var。

条件语法

在 if 中条件,也就是if(P)中的命题 P 可以实现丰富的功能,更详细的讨论可以参考CMake官方文档

因为 if 语句出现的太早了,导致if(P)的语法看起来非常奇怪: 尝试对一个变量名称自动求值,if(${P})。 如果希望处理一个可能是变量名的字符串,建议使用双引号if("${P}"),这会抑制 if 的自动求值。总之要么用if(P)要么用if("${P}")

基本变量

P 可以是最基本的常量,字符串或者变量名。

  1. 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 等都是可以的
    • 其它情形会被视作一个变量或一个字符串进行处理
  2. if(<variable>): P 是一个变量的名称(而非变量的值)
    • 变量已定义,并且变量的值不是上述 False 常量的情形 \(\Rightarrow\) True
    • 变量已定义,但是变量的值是上述 False 常量的情形 \(\Rightarrow\) False
    • 变量未定义 \(\Rightarrow\) False
    • 上述规则对宏结构不使用,对环境变量也不使用(环境变量的名称总是得到 False)
  3. if(<string>): P 是字符串
    • 可以被解析为 True 常量的字符串 \(\Rightarrow\) True
    • 通常情形下,其它的字符串 \(\Rightarrow\) False

常见的例如

1
2
3
4
5
6
7
8
set(A "") # 空字符串
...

if(A)
# 如果仍然是空字符串
else()
# 如果被改动,被定义并解析为True
endif()

逻辑判断

P 可以是一些简单的逻辑判断

1
2
3
4
5
6
7
8
# 取反运算
if(NOT <condition>)

# 与运算
if(<cond1> AND <cond2>)

# 或运算
if(<cond1> OR <cond2>)

可以支持基于与或非的复杂组合,例如

1
if((condition1) AND (condition2 OR (condition3)))

注意:

  • 可以使用小括号改变计算优先级
  • NOT 的优先级比 AND/OR 更高
  • 对于 AND、OR 计算顺序从左到右,并且不使用任何短路操作:x AND y在 x 不正确时仍然解析 y

存在性判断

  • if(COMMAND command-name): 判断这个 command-name 是否属于命令、可调用的宏或者函数的名称,则返回 True
  • if(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
2
3
4
5
6
7
8
9
10
# 小于
if(<variable|string> LESS <variable|string>)
# 大于
if(<variable|string> GREATER <variable|string>)
# 等于
if(<variable|string> EQUAL <variable|string>)
# 小于或等于
if(<variable|string> LESS_EQUAL <variable|string>)
# 大于或等于
if(<variable|string> GREATER_EQUAL <variable|string>)
  • 字符串的比较(字典序)
1
2
3
4
5
if(<variable|string> STRLESS <variable|string>)
if(<variable|string> STRGREATER <variable|string>)
if(<variable|string> STREQUAL <variable|string>)
if(<variable|string> STRLESS_EQUAL <variable|string>)
if(<variable|string> STRGREATER_EQUAL <variable|string>)
  • 版本号比较(版本号格式为major[.minor[.patch[.tweak]]],省略的部分被视为零)
1
2
3
4
5
if(<variable|string> VERSION_LESS <variable|string>)
if(<variable|string> VERSION_GREATER <variable|string>)
if(<variable|string> VERSION_EQUAL <variable|string>)
if(<variable|string> VERSION_LESS_EQUAL <variable|string>)
if(<variable|string> VERSION_GREATER_EQUAL <variable|string>)

路径与文件判断

比较简略,因为目录和文件需要考虑平台和格式等,细节比较多,用到再查文档吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 完整路径是否存在,这里~开头的还不行
if(EXISTS path-to-file-or-directory)

# 两个完整路径下的文件比较时间戳
if(file1 IS_NEWER_THAN file2)

# 完整路径是否是一个目录
if(IS_DIRECTORY path-to-directory)

# 完整路径是不是绝对路径
if(IS_ABSOLUTE path)
# 对于Windows,要求路径以盘符开始
# 对于Linux,要求路径以~开始
# 空路径视作false

foreach 循环

基本用法

foreach 循环最基本的用法就是遍历一个列表中的所有变量,此时需要使用${Var}先解析列表的值,例如

1
2
3
4
5
6
set(A 1;2;3;4)

foreach(X ${A})
message("X=${X}")
endforeach()
# 1,2,3,4

由于我们已经把列表解析了,所以下面的用法也是一样的

1
2
3
4
foreach(x a b c)
message("x=${x}")
endforeach()
# a b c

更本质地说,foreach 基本用法中,cmake 会把第一个位置的字符串定义为循环变量,把剩下的字符串数组视作迭代的列表。

等差数列遍历

foreach 支持基于等差数列的遍历,可以使用foreach(... RANGE ...)命令, 其中range n包括从 0 到 n 的自然数序列,range a b c可以控制起点终点和步长,语法为

1
2
foreach(<loop_var> RANGE <stop>)
foreach(<loop_var> RANGE <start> <stop> [<step>])

例如

1
2
3
4
5
6
7
8
9
foreach(X RANGE 10)
message("X=${X}")
endforeach()
# 0,1,...,10

foreach(X RANGE 0 10 2)
message(STATUS "X=${X}")
endforeach()
# 0,2,4,8,10

遍历多个列表

foreach 可以同时在多个列表中遍历,需要使用关键词foreach(... IN ZIP_LISTS ...),总的迭代次数以最长的列表为准,此时较短的列表对应取空值。

方式一,要求循环变量和列表的个数一致,此时每一个循环变量对应在一个列表中遍历,互不干扰

1
foreach(<loop_vars> IN ZIP_LISTS <lists>)

例如

1
2
3
4
5
6
7
8
foreach(en ba IN ZIP_LISTS English Bahasa)
message("en=${en}, ba=${ba}")
endforeach()

# en=one, ba=satu
# en=two, ba=dua
# en=three, ba=tiga
# en=four, ba=

方式二,通过单个变量loop_var的多个分量loop_var_N实现

1
foreach(<loop_var> IN ZIP_LISTS <lists>)

这里只用一个 loop_var,来承接对后面多个列表的遍历,它通过分量 loop_var_N 变量记录对应列表的当前项,例如

1
2
3
4
5
6
7
8
9
10
11
list(APPEND English one two three four)
list(APPEND Bahasa satu dua tiga)

foreach(num IN ZIP_LISTS English Bahasa)
message("num_0=${num_0}, num_1=${num_1}")
endforeach()

# num_0=one, num_1=satu
# num_0=two, num_1=dua
# num_0=three, num_1=tiga
# num_0=four, num_1=

其它

和其它语言一样,cmake 支持使用break()命令提前终止循环,使用continue()命令可用于立即开始下一次迭代。

cmake 还支持 while 循环,但是感觉用处不大,因为 cmake 不需要很复杂的循环逻辑,foreach 提供基本的遍历已经足够了。

1
2
3
while(<condition>)
<commands>
endwhile()

例如

1
2
3
4
5
set(i 0)
while(i LESS <n>)
# ...
math(EXPR i "${i} + 1")
endwhile()

函数

函数的语法结构如下:定义名为<name>的函数,该函数接收名为<arg1>...的参数,<commands>表示函数定义的功能,在调用函数之前不会被执行。

1
2
3
function(<name> [<arg1> ...])
<commands>
endfunction()

可以使用<name>(...)调用执行函数

  • 函数体的内部存在一个独立的作用域,在内部定义和使用的变量默认都是"局部"的,不会影响到函数体外
  • 调用函数时,对函数名称的大小写不敏感,Foo()FOO()foo()效果一样,建议使用全小写
  • 函数的参数并没有形式和个数的要求,可以传入任意多的参数
  • 函数会把传入的参数视作普通的字符串,把它作为指定名称的参数变量的值,而不是视作传入一个变量进行复制
  • 函数执行时,会首先对函数体内部使用到的函数参数进行一轮替换,然后逐个执行命令

cmake 语法设计非常混乱,最基本的对于函数的参数都有很多种调用的方法,这些方法可以混合使用,不会互相冲突。

参数方式(一)

第一种方式,使用 cmake 默认提供的函数参数代词(注意,它们不是通常的变量,因为在调用函数时会直接全部替换掉)

  • ARGC: 实际传入的参数的个数,与函数定义时预期的参数个数无关
  • ARGV: 实际传入的参数全体(以列表的形式)
  • ARGV#: 实际传入的第#个参数,计数从 0 开始的每一个参数,例如 ARGV0 为第一个参数,ARGV1 为第二个参数,依次类推(注意,对于计数超过ARGCARGV#,属于未定义的量)
  • ARGN: 预料之外的参数,定义宏(函数)时参数为 2 个,实际传了 4 个,则 ARGN 代表剩下的两个

例如,定义函数如下

1
2
3
4
5
6
function(test)
message(STATUS "ARGC=${ARGC}")
message(STATUS "ARGV=${ARGV}")
message(STATUS "ARGV2=${ARGV2}")
message(STATUS "ARGN=${ARGN}")
endfunction(test)

没有给出一个预期的参数,三组测试结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
test(1 2 3)
# -- ARGC=3
# -- ARGV=1;2;3
# -- ARGV2=3
# -- ARGN=1;2;3

test()
# -- ARGC=0
# -- ARGV=
# -- ARGV2=
# -- ARGN=

test(4 5)
# -- ARGC=2
# -- ARGV=4;5
# -- ARGV2=
# -- ARGN=4;5

参数方式(二)

第二种方式,给 function 提供默认的形参名,此时传入的参数会依次分配给对应的形参,多的部分还是只能通过ARGN获取。

1
2
3
function(test2 var)
message("var=${var}")
endfunction()

测试结果如下

1
2
3
4
5
6
test2("abc") # var=abc
test2("abc" "d") # var=abc 第二个参数丢失了,只能使用ARGN获取

set(a 10)
test2(a) # var=a
test2(${a}) # var=10

这个例子说明:函数只会把解析后传入的形参作为纯粹的字符串常量,视作实参变量的值,并不是视作变量的重命名。如果坚持使用 test2(a),那么在 test2 内部,必须使用${${var}}双层调用,来获取 a 的值。(不建议)

注意,${var}不同于"${var}"(其中的 var 是一个列表),在函数调用时因为解析的逻辑导致不同的结果

1
2
3
4
5
6
7
8
9
function(PrintVar var)
message("${var}")
endfunction()

PrintVar(${specialStr}) # aaa
# 这里分开传入,视作多个参数

PrintVar("${specialStr}") # aaa;bbb
# 这里作为一个整体传入,视作一个参数

即列表作为参数时

列表 var 作为参数传入函数时:

  • 调用时写成 ${var},会被展开识别为多个参数

  • 调用时写成 "${var}",会被打包作为一个列表整体,分配给一个参数

  • 调用时写成 var,在函数内需要两层解析${${var}}才能得到值列表

1
2
3
4
5
6
7
8
9
10
11
12
13
function(hello var)
message("var=${var}|${${var}}")
endfunction()

set(A x y z)

hello(A)
hello(${A})
hello("${A}")

# var=A|x;y;z
# var=x|
# var=x|y;z,

参数方式(三)

推荐在复杂参数情形下,使用cmake_parse_arguments命令来解析函数或宏的参数,因为这个命令非常强大。 (不推荐与其他参数方式混用,不要在 function 命令中指定参数名称,而是放到函数体内部,使用cmake_parse_arguments命令实现)

语法形式为

1
2
cmake_parse_arguments(<prefix> <options> <one_value_keywords>
<multi_value_keywords> <args>...)

我们通过例子来解释这个命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function(test)

# 选项(布尔参数)的关键词列表
set(argop "OA;OB;OC")

# 单值参数的关键词列表
set(arg "SA;SB;SC")

# 列表参数的关键词列表
set(arglist "LA;LB;LC")

cmake_parse_arguments("ARG" "${argop}" "${arg}" "${arglist}" ${ARGN})
message("-----------")
message("${ARGV}")
message("-----------")
message("ARG_OA=${ARG_OA}")
message("ARG_OB=${ARG_OB}")
message("ARG_OC=${ARG_OC}")

message("ARG_SA=${ARG_SA}")
message("ARG_SB=${ARG_SB}")
message("ARG_SC=${ARG_SC}")

message("ARG_LA=${ARG_LA}")
message("ARG_LB=${ARG_LB}")
message("ARG_LC=${ARG_LC}")
message("-----------")
endfunction()

其中的"ARG"是我们指定的前缀,避免名称冲突,此时 cmake 会自动创建若干名称为ARG_XXX的变量,来承接传入的参数。

  • 调用函数时,如果传入了 OA 或者其它的 argop 中的内容,则定义ARG_OA变量为真
1
2
3
test(OA ...)

# ARG_OA=True
  • 调用函数时,如果传入了 SA 并且附带一个值 value,则定义ARG_SA变量,它的值即为 value
1
2
3
test(... SA "abc")

# ARG_SA="abc"
  • 调用函数时,如果传入了 LA 并且附带一个列表 values,则定义ARG_LA变量,它的值即为 values
1
2
3
test(... LA "a;b;c")

# ARG_SA=a;b;c
  • 习惯在命令的最后使用${ARGN},承接一些额外的参数

测试结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 第一个,不使用任何参数
# test()
-----------

-----------
ARG_OA=FALSE
ARG_OB=FALSE
ARG_OC=FALSE
ARG_SA=
ARG_SB=
ARG_SC=
ARG_LA=
ARG_LB=
ARG_LC=
-----------

# 第二个,使用选项OA,传入LA对应的值列表
# test(OA LA a;b;c)
-----------
OA;LA;a;b;c
-----------
ARG_OA=TRUE
ARG_OB=FALSE
ARG_OC=FALSE
ARG_SA=
ARG_SB=
ARG_SC=
ARG_LA=a;b;c
ARG_LB=
ARG_LC=
-----------

# 第三个,使用选项OB,传入LA的列表,再传入SA的值
# test(OB LA "a;b;c" SA "e")
-----------
OB;LA;a;b;c;SA;e
-----------
ARG_OA=FALSE
ARG_OB=TRUE
ARG_OC=FALSE
ARG_SA=e
ARG_SB=
ARG_SC=
ARG_LA=a;b;c
ARG_LB=
ARG_LC=
-----------

以上是正确使用三类参数的情形,可以发现我们并不需要按照选项、单值参数和列表参数的顺序,只需要在正确的关键字后面跟上值或者列表即可。

换言之,cmake_parse_arguments命令使得我们可以用无序的参数键值对的形式,向调用的函数传入指定名称指定值的参数。

1
2
cmake_parse_arguments(<prefix> <options> <one_value_keywords>
<multi_value_keywords> <args>...)

cmake_parse_arguments会解析传入的所有参数,按照指定的关键词和逻辑去分析捕获参数,并产生带有指定前缀的一组新变量:

  • <prefix>是我们希望给捕获产生的所有新变量添加的前缀
  • 如果遇到<options>中的关键词,则对应产生的新变量为 true;否则默认为 false
  • 如果遇到<one_value_keywords>中的关键词,则把随后的一个值赋给产生的新变量;否则默认为空(未定义)
  • 如果遇到<multi_value_keywords>中的关键词,则把随后的一个列表赋给产生的新变量,直到遇见下一个关键词;否则默认为空(未定义)
  • 最后的<args>...部分,习惯上都使用${ARGN}
  • 如果给的参数无法被 cmake 按照上述逻辑捕获,仍然可以通过传统的ARGN方式获得:由于我们没有在 function 命令指定任何参数名,其实所有的参数都被扔进了ARGNARGV

变量作用域

在函数内部直接使用 set 命令不会影响到函数体之外,在退出函数体之后变量即失效

1
2
3
4
function(foo)
set(A 100)
endfunction()
# 在内部对A的定义无效,对于外层来说,无事发生

可以使用 PARENT_SCOPE,表明退出这一层作用域后,变量仍然存在

1
2
3
4
function(foo)
set(A 100 PARENT_SCOPE)
endfunction()
# 在内部对A的定义仍然有效

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
message("(1)A=${A}")
foo1()
message("(2)A=${A}")

function(foo2)
set(A 200 PARENT_SCOPE)
endfunction()

message("(3)A=${A}")
foo2()
message("(4)A=${A}")

# (1)A=10
# (2)A=10
# (3)A=10
# (4)A=200

这里体现的不仅仅是函数变量,而是所有的普通变量都具有的变量作用域机制:(与之相对的,缓存变量是全局的)

  • 通过 add_subdirectory()进入新的 CMakeLists.txt 具有独立的变量作用域。
  • 通过 include 导入的.cmake 文件没有独立的变量作用域。
  • 自定义函数具有独立的变量作用域。
  • 自定义的宏没有独立的变量作用域。

普通变量对子作用域可见,但是子作用域中的变量不会影响到父作用域,相当于定义了同名变量并覆盖,除非使用 PARENT_SCOPE 选项。

函数返回值

cmake 的函数可以在函数体的任何地方直接返回,使用return()即可,但是 cmake 的函数语法有一个致命的问题,居然不支持返回值!在 CMake 中为了得到函数的返回值,我们必须把一个变量传给函数,然后通过提升作用域的方式实现,例如

1
2
3
4
5
6
7
8
function(test_return rst arg)
math(EXPR argnew "${arg}+1")
set(${rst} ${argnew} PARENT_SCOPE)
endfunction()

set(b 10)
test_return(b 1)
message("b=${b}") # b=2

还可以这么干,直接使用一个变量负责传入传出,需要两层解析来获取传入的值,再使用PARENT_SCOPE把变量传回去

1
2
3
4
5
6
7
8
9
function(test_return2 rst)
set(tmp "${${rst}}")
math(EXPR tmp "${tmp}+1")
set(${rst} ${tmp} PARENT_SCOPE)
endfunction()

set(b 10)
test_return2(b)
message("b=${b}") # b=11

函数执行状态

在函数体内,可以使用如下的特殊变量,获取当前的函数执行状态

  • 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
2
3
macro(<name> [<arg1> ...])
<commands>
endmacro()

宏和函数很相似,宏的名称是大小写不敏感的,建议完全小写。宏也支持使用ARGV之类的默认参数名称,或者使用指定参数名称的方式,再或者cmake_parse_arguments的方式。

我们重点关注宏和函数的不同点:

  • 宏并没有像函数一样单独开辟一个作用域,而是简单地执行了两步操作
    1. 完成对参数的字符串替换
    2. 把命令部分拷贝过来执行
  • 在宏的内部禁止使用return()命令,因为这个命令的效果不是退出宏,而是退出上一层
  • 在宏的内部没有单独作用域,而且ARGV0不是通常的变量,只是类似于单纯的字符串替换,因此不能使用如下的语法
1
2
if(ARGV0) # 在函数中可以,但是在宏内部错误,if不会解析ARGV0
if(${ARGV0}) # 正确,注意在进入宏之后,这里就完成了对ARGV0的字符串展开

模块

模块就是以 xxx.cmake 结尾的文件,通常将一些通用的函数或宏封装到到一个指定的文件中,然后通过include(xxx)方式引用,可以达到代码复用的目的(但是并不会创建独立的变量作用域)。 模块既可以被 CMakeLists.txt 引用,也可以被其它模块引用。

模块的查找可以是从当前位置出发的相对路径,此时需要带文件后缀,例如include(cmake/xxx.cmake);也可以从CMAKE_MODULE_PATH查找,此时只需要include(xxx),在查找前可以修改这个变量,让 CMake 顺利找到模块。