CMake(五):控制流

对于大多数CMake项目来说,一个常见的需求是只在特定的情况下应用一些步骤。例如,项目可能只希望在特定的编译器或针对特定平台构建时使用特定的编译器标志。在其他情况下,项目可能需要迭代一组值,或者不断重复某些步骤,直到满足某个条件。CMake很好地支持了这些流控制示例,大多数软件开发人员应该都很熟悉这些方法。普遍存在的if()命令提供了预期的if-then-else行为,循环是通过foreach()和while()命令提供的。这三个命令提供了大多数编程语言实现的传统行为,但它们也添加了特定于CMake的特性。

5.1 if()命令

​ if()命令的现代形式如下(可以提供多个elseif()子句):

if(expression1)
  # commands ...
elseif(expression2)
  # commands ...
else()
  # commands ...
endif()

​ CMake的早期版本要求expression1作为else()和endif()子句的参数被重复,但这在CMake 2.8.0之后就不再要求了。虽然使用这种旧形式的项目和示例代码仍然很常见,但不鼓励使用这种形式的新项目,因为它读起来有点混乱。新项目应该保留else()和endif()参数为空,如上所示。

​ if()和elseif()命令中的表达式可以采用各种不同的形式。CMake提供了传统的布尔逻辑以及各种其他条件,如文件系统测试、版本比较和事物存在性测试。

(1)基本的表达式

​ 所有表达式中最基本的是一个常量:

if(value)

​ CMake的真与假逻辑比大多数编程语言要复杂一些。对于单个未加引号的常量,规则如下:

  • 如果value是一个值为1、ON、YES、TRUE、Y或非零的未引号常量,则它被视为TRUE。测试不区分大小写。
  • 如果value是一个未加引号的常量,值为0、OFF、NO、FALSE、N、IGNORE、NOTFOUND、空字符串或以-NOTFOUND结尾的字符串,则将其视为FALSE。同样,该测试不区分大小写。
  • 如果上述两种情况都不适用,它将被视为一个变量名(或者可能是一个字符串),并按照下面的描述进一步计算。

​ 在下面的例子中,为了演示的目的,只显示了命令的if(…)部分,对应的body和endif()被省略:

# Examples of unquoted constants
if(YES)
if(0)
if(TRUE)
# These are also treated as unquoted constants because the
# variable evaluation occurs before if() sees the values
set(A YES)
set(B 0)
if(${A}) # Evaluates to true
if(${B}) # Evaluates to false
# Does not match any of the true or false constants, so proceed
# to testing as a variable name in the fall through case below
if(someLetters)
# Quoted value, so bypass the true/false constant matching
# and fall through to testing as a variable name or string
if("someLetters")

​ CMake文档中提到的失败案例如下所示:

if(<variable|string>)

​ 在实践中,if-expression的意思是:

  • (可能是未定义的)变量的未加引号的名称。
  • 一个引用字符串。

​ 当使用未带引号的变量名时,变量的值将与false常量进行比较。如果这些值都不匹配,则表达式的结果为真。未定义的变与空字符串等价,该字符串匹配其中一个false常量,因此结果为false。

# Common pattern, often used with variables defined
# by commands such as option(enableSomething "...")
if(enableSomething)
  # ...
endif()

​ 当if-expression是一个带引号的字符串时,行为就更复杂了:

  • 在CMake 3.1或更高版本中,带引号的字符串总是计算为false,而不管字符串的值是什么(但这可以被策略设置覆盖)
  • 在CMake 3.1之前,如果字符串的值与现有变量的名称匹配,那么引用的字符串将被该变量的名称(未引用的)替换,然后重复测试。

​ 上述两种情况可能会让开发人员感到意外,但至少CMake 3.1的行为总是可以预测的。当字符串值碰巧与变量名匹配时,3.1版本之前的行为偶尔会导致意想不到的字符串替换,可能是在远离项目的某个地方定义的字符串。用引号括起来的值可能引起混淆,这意味着通常建议避免在if(something)形式中使用用引号括起来的参数。

(2)逻辑运算符

​ CMake支持常用的AND, OR和NOT逻辑运算符,以及控制优先顺序的括号。

# Logical operators
if(NOT expression)
if(expression1 AND expression2)
if(expression1 OR expression2)
# Example with parentheses
if(NOT (expression1 AND (expression2 OR expression3)))

​ 按照通常的约定,括号内的表达式首先求值,从最里面的括号开始。

(3)比较测试

​ CMake将比较测试分为三个不同的类别:数字、字符串和版本号,但语法形式都遵循相同的模式:

if(value1 OPERATOR value2)

​ 两个操作数value1和value2可以是变量名或值(可能用引号括起来)。如果一个值与一个已定义变量的名称相同,则它将被视为一个变量。否则,它将直接作为字符串或值处理。不过,引用的值同样具有类似于基本一元表达式的模糊行为。在CMake 3.1之前,匹配变量名的带引号字符串将被该变量的值所替换。CMake 3.1及以后的行为使用引用的值而不进行替换,这正是开发人员直观地期望的。

这三个比较类别都支持同一组操作,但每个类别的操作符名称不同。下表总结了支持的操作符:

NumericStringVersion numbers
LESSSTRLESSVERSION_LESS
GREATERSTRGREATERVERSION_GREATER
EQUALSTREQUALVERSION_EQUAL
LESS_EQUAL*STRLESS_EQUAL*VERSION_LESS_EQUAL*
GREATER_EQUAL*STRGREATER_EQUAL*VERSION_GREATER_EQUAL*

*仅在CMake 3.7及以后版本中可用。

​ 数值比较的工作正如人们所期望的那样,将左边的值与右边的值比较。然而,请注意,如果其中任何一个操作数不是数字,并且当值包含的不仅仅是数字时,它的行为不完全符合官方文档,那么CMake通常不会引发错误。根据数字和非数字的混合,表达式的结果可能为真或假。

# Valid numeric expressions, all evaluating as true
if(2 GREATER 1)
if("23" EQUAL 23)
set(val 42)
if(${val} EQUAL 42)
if("${val}" EQUAL 42)
# Invalid expression that evaluates as true with at
# least some CMake versions. Do not rely on this behavior.
if("23a" EQUAL 23)

版本号比较有点像数字比较的增强形式。版本号假设为major[.minor[.patch[. patch]。tweak]]],其中每个组件预期为一个非负整数。比较两个版本号时,先比较主要部分。只有当主要部分相等时,才会比较次要部分(如果存在的话),以此类推。缺失的组件被视为零。在下面所有的例子中,表达式的计算结果都是true:

if(1.2 VERSION_EQUAL 1.2.0)
if(1.2 VERSION_LESS 1.2.3)
if(1.2.3 VERSION_GREATER 1.2 )
if(2.0.1 VERSION_GREATER 1.9.7)
if(1.8.2 VERSION_LESS 2 )

​ 版本号比较具有与数字比较相同的健壮性注意事项。每个版本组件应该是一个整数,但是如果这个限制不成立,那么比较结果基本上是没有定义的。

​ 对于字符串,值按字典顺序进行比较。没有对字符串的内容做任何假设,但是要注意前面描述的变量/字符串替换情况的可能性。字符串比较是发生这种意外替换的最常见情况之一。

​ CMake还支持用正则表达式测试字符串:

if(value MATCHES regex)

​ 该值再次遵循上面定义的变量或字符串规则,并与regex正则表达式进行比较。如果值匹配,表达式的计算结果为true。虽然CMake文档没有为if()命令定义支持的正则表达式语法,但它在其他地方为其他命令定义了正则表达式语法(例如,请参阅string()命令文档)。本质上,CMake只支持基本的正则表达式语法。

​ 括号可以用来捕获匹配值的部分。该命令将设置名称为CMAKE_MATCH_的变量,其中是要匹配的组。整个匹配的字符串存储在组0中。

if("Hi from ${who}" MATCHES "Hi from (Fred|Barney).*")
  message("${CMAKE_MATCH_1} says hello")
endif()

(4)文件系统测试

​ CMake还包括一组测试,可以用来查询文件系统。支持以下表达式:

if(EXISTS pathToFileOrDir)
if(IS_DIRECTORY pathToDir)
if(IS_SYMLINK fileName)
if(IS_ABSOLUTE path)
if(file1 IS_NEWER_THAN file2)

​ 上述内容在很大程度上应该是不言自明的,但也有一些要点需要注意。特别是,如果其中一个文件缺失,或者两个文件都有相同的时间戳(这包括如果为file1和file2都给出了相同的文件),则IS_NEWER_THAN操作符返回true。因此,在执行实际的IS_NEWER_THAN测试之前,测试是否存在file1和file2是很正常的,因为IS_NEWER_THAN的结果(其中两个文件都丢失了)通常都不是开发人员直观地期望的结果。当使用IS_NEWER_THAN时,还应该给出完整的路径,因为相对路径的行为没有很好地定义。

​ 另一点需要注意的是,与大多数其他if表达式不同,没有${}的文件系统操作符都不会执行任何变量/字符串替换,无论是否使用引号。

(5)存在测试

​ 最后一类if表达式支持测试是否存在各种CMake实体。它们在更大、更复杂的项目中特别有用,在这些项目中,有些部分可能存在,也可能不存在或是否启用。

if(DEFINED name)
if(COMMAND name)
if(POLICY name)
if(TARGET name)
if(TEST name) # Available from CMake 3.4 onward

​ 如果在发出if命令的地方存在指定名称的实体,上面的每一个都将返回true。

  • DEFINED

    如果存在指定名称的变量,则返回true。变量的值无关紧要,只测试它的存在性。这也可以用来检查是否定义了特定的环境变量:

if(DEFINED SOMEVAR) # Checks for a CMake variable
if(DEFINED ENV{SOMEVAR}) # Checks for an environment variable
  • COMMAND

    测试指定名称的CMake命令、函数或宏是否存在。这对于在尝试使用某事物之前检查它是否被定义是非常有用的。对于CMake提供的命令,最好是测试CMake版本,但是对于项目提供的函数和宏,使用COMMAND测试来测试它们是否存在可能会很有用。

  • POLICY

    测试CMake是否知道某个策略。策略名称通常为CMPxxxx形式,其中xxxx部分始终是四位数字。

  • TARGET

    如果指定名称的CMake目标已经被add_executable(),add_library()或add_custom_target()命令之一定义,则返回true。目标可以定义在任何目录中,只要它在执行if测试的地方是已知的。这个测试在复杂的项目层次结构中特别有用,在这个层次结构中有其他的外部项目,并且这些项目可能共享共同的依赖子项目(例如,这种if测试可以用来检查在尝试创建目标之前是否已经定义了)。

  • TEST

    如果指定名称的CMake测试之前已经被add_test()命令定义过,则返回true。

​ 最后一个存在测试在CMake 3.5及更高版本中可用:

if(value IN_LIST listVar)

​ 如果变量listVar包含指定的值,这个表达式将返回true,其中value遵循通常的变量或字符串规则,但listVar必须是列表变量的名称。

(6)常见的例子

​ if()的一些用法非常普遍,值得特别提及。其中许多依赖于预定义的CMake变量来实现它们的逻辑,特别是与编译器和目标平台相关的变量。不幸的是,经常会看到这样的表达式基于错误的变量。例如,考虑一个有两个c++源文件的项目,一个用于用Visual Studio编译器或与之兼容的编译器(例如Intel)构建,另一个用于用所有其他编译器构建。这样的逻辑通常是这样实现的:

if(WIN32)
  set(platformImpl source_win.cpp)
else()
  set(platformImpl source_generic.cpp)
endif()

​ 虽然这可能适用于大多数项目,但它实际上并没有表达正确的约束。例如,考虑一个在Windows上构建但使用MinGW编译器的项目。对于这种情况,source_generic.cpp可能是更合适的源文件。以上可以更准确地实现如下:

if(MSVC)
  set(platformImpl source_msvc.cpp)
else()
  set(platformImpl source_generic.cpp)
endif()

​ 另一个例子涉及到基于CMake生成器的条件行为。特别是,CMake在使用Xcode生成器构建时提供了其他生成器不支持的额外特性。项目有时会假设,为macOS构建意味着将使用Xcode生成器,但这并不是必须的(通常也不是)。有时会使用以下错误的逻辑:

if(APPLE)
  # Some Xcode-specific settings here...
else()
  # Things for other platforms here...
endif()

​ 再次强调,这似乎是正确的,但如果开发人员试图在macOS上使用不同的生成器(例如忍者或Unix Makefiles),逻辑就会失败。用APPLE表达的表达式测试平台并不能表达正确的条件,应该测试CMake生成器:

if(CMAKE_GENERATOR STREQUAL "Xcode")
  # Some Xcode-specific settings here...
else()
  # Things for other CMake generators here...
endif()

​ 上面的例子都是测试平台而不是约束实际涉及到的实体。这是可以理解的,因为平台是最容易理解和测试的东西之一,但是使用它而不是更精确的约束会不必要地限制开发人员的生成器选择,或者可能导致完全错误的行为。

​ 另一个常见的例子(这次使用得很恰当)是根据是否设置了特定的CMake选项来有条件地包含目标。

option(BUILD_MYLIB "Enable building the myLib target")
if(BUILD_MYLIB)
  add_library(myLib src1.cpp src2.cpp)
endif()

​ 更复杂的项目经常使用上面的模式,有条件地包含子目录或基于CMake选项或缓存变量执行各种其他任务。开发人员可以打开/关闭该选项,或者将该变量设置为非默认值,而不必直接编辑CMakeLists.txt文件。这对于由持续集成系统等驱动的脚本构建特别有用,这些系统可能想要启用或禁用构建的某些部分。

5.2 循环

​ 在许多CMake项目中,另一个常见的需求是对一组项或一组值执行一些操作。或者,某些操作可能需要重复执行,直到满足特定条件。CMake很好地满足了这些需求,通过添加一些功能来提供传统的行为,使使用CMake特性变得更容易一些。

(1)foreach

​ CMake提供了foreach()命令来允许项目迭代一组项或值。foreach()有几种不同的形式,其中最基本的是:

foreach(loopVar arg1 arg2 ...)
  # ...
endforeach()

​ 在上面的形式中,对于每个argN值,都会将loopVar设置为该参数,并执行循环体。不执行变量/字符串测试,参数完全按照指定的值使用。与其显式地列出每一项,参数也可以通过一个或多个列表变量来指定,使用命令的更一般形式:

foreach(loopVar IN [LISTS listVar1 ...] [ITEMS item1 ...])
  # ...
endforeach()

在这种更一般的形式中,仍然可以使用ITEMS关键字指定单个参数,但是LISTS关键字允许指定一个或多个列表变量。使用这种更通用的形式时,必须提供item和/或list中的一个或两个。当两者都提供时,item必须出现在list之后。允许listVarN列表变量保存一个空列表。举个例子可以帮助你理解这个更一般的用法。

set(list1 A B)
set(list2)
set(foo WillNotBeShown)
foreach(loopVar IN LISTS list1 list2 ITEMS foo bar)
  message("Iteration for: ${loopVar}")
endforeach()

image-20220110213337959

​ foreach()命令还支持对一系列数值进行更像c语言的迭代:

foreach(loopVar RANGE start stop [step])

​ 当使用foreach()的RANGE形式时,循环被执行,loopVar设置为start到stop范围内的每个值(包括)。如果提供了step选项,则在每次迭代后将该值添加到前一个值,当结果大于stop时,循环停止。

​ RANGE表单也只接受这样一个参数:

foreach(loopVar RANGE value)

​ 这相当于foreach(loopVar RANGE 0 value),这意味着循环体将执行(value + 1)次。这是不幸的,因为更直观的期望可能是循环体执行值的时间。出于这个原因,避免使用第二个RANGE形式,而是显式地指定start和stop值,可能会更清楚。

​ 类似于if()和endif()命令的情况,在CMake的早期版本中(即2.8.0之前),foreach()命令的所有形式都需要将loopVar指定为endforeach()的参数。这再次损害了可读性,而且几乎没有什么好处,所以不鼓励在新项目中使用endforeach()指定loopVar。

(2)while()

​ CMake提供的另一个循环命令是while():

while(condition)
  # ...
endwhile()

​ 测试条件,如果它的计算结果为true(遵循与if()语句中的表达式相同的规则),则执行循环体。这将重复执行,直到condition的计算结果为false或循环提前退出。同样,在2.8.0之前的CMake版本中,这个条件必须在while()命令的末尾重复,但这不再是必要的,并且对于新项目是不鼓励的。

(3)Interrupting Loops

​ while()和foreach()循环都支持使用break()提前退出循环,或者使用continue()跳到下一次迭代的开始。这些命令的行为与C语言中同名的类似命令一样,并且都只对最内部的封闭循环进行操作。下面的示例演示了该行为。

foreach(outerVar IN ITEMS a b c)
  unset(s)
  foreach(innerVar IN ITEMS 1 2 3)
  # Stop inner loop once string s gets long
  list(APPEND s "${outerVar}${innerVar}")
  string(LENGTH s length)
  if(length GREATER 5)
  break() ①
  endif()
  # Do no more processing if outer var is "b"
  if(outerVar STREQUAL "b")
  continue() ②
  endif()
  message("Processing ${outerVar}-${innerVar}")
  endforeach()
  message("Accumulated list: ${s}")
endforeach()

​ ①提前结束innerVar for循环

​ ②结束当前的innerVar迭代,并移动到下一个innerVar项目。

image-20220110214238531
相关代码:https://gitee.com/jiangli01/cmake-learning
更多请关注微信公众号【Hope Hut】:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值