CMake(七):函数和宏

2023-11-10

回顾到目前为止涉及的材料,CMake的语法已经开始看起来很像一门编程语言。它支持变量、if-then-else逻辑、循环和包含要处理的其他文件。毫无疑问,CMake还支持常用的函数和宏编程概念。就像它们在其他编程语言中的角色一样,函数和宏是项目和开发人员扩展CMake功能和以自然的方式封装重复任务的主要机制。它们允许开发者定义可重用的CMake代码块,可以像普通的内置CMake命令一样调用它们。它们也是CMake自己模块系统的基石。

7.1 基础

CMake中的函数和宏与C/C++中同名的函数和宏有着非常相似的特性。函数引入了一个新的作用域,函数参数成为函数体内部可访问的变量。另一方面,宏有效地将它们的主体粘贴到调用点,宏参数被替换为简单的字符串替换。这些行为反映了C/ c++中函数和#define宏的工作方式。一个CMake函数或宏的定义如下:

function(name [arg1 [arg2 [...]]])
  # Function body (i.e. commands) ...
endfunction()
macro(name [arg1 [arg2 [...]]])
  # Macro body (i.e. commands) ...
endmacro()

一旦定义好,函数或宏就会像其他CMake命令一样被调用。然后在调用点执行函数或宏的主体。例如:

function(print_me)
  message("Hello from inside a function")
  message("All done")
endfunction()
# Called like so:
print_me()

image-20220112162712140

如上所示,name参数定义了用于调用函数或宏的名称,它应该只包含字母、数字和下划线。名称将不区分大小写,因此大写/小写约定更多的是一种风格问题(CMake文档遵循这样的约定,命令名称都是小写的,单词之间用下划线分隔)。CMake的早期版本要求名称作为endfunction()或endmacro()的参数重复,但新项目应该避免这种情况,因为它只会增加不必要的混乱。

7.2 函数处理要点

函数和宏的参数处理是相同的,除了一个非常重要的区别。对于函数,每个参数都是一个CMake变量,并且具有CMake变量的所有常见行为。例如,它们可以作为变量在if()语句中进行测试。相比之下,宏参数是字符串替换,因此无论用作宏调用的参数是什么,本质上都被粘贴到该参数在宏体中的任何位置。如果在If()语句中使用宏参数,它将被视为字符串而不是变量。下面的例子及其输出说明了两者的区别:

function(func arg)
  if(DEFINED arg)
  message("Function arg is a defined variable")
  else()
  message("Function arg is NOT a defined variable")
  endif()
endfunction()
macro(macr arg)
  if(DEFINED arg)
  message("Macro arg is a defined variable")
  else()
  message("Macro arg is NOT a defined variable")
  endif()
endmacro()
func(foobar)
macr(foobar)

image-20220112163736529

除了这个区别之外,在参数处理方面,函数和宏都支持相同的特性。函数定义中的每个参数都作为它所表示的参数的大小写敏感标签。对于函数,这个标签就像一个变量,而对于宏,它就像一个字符串替换。该参数的值可以在函数或宏体中使用常用的变量表示法访问,即使宏参数在技术上不是变量。

function(func myArg)
  message("myArg = ${myArg}")
endfunction()
macro(macr myArg)
  message("myArg = ${myArg}")
endmacro()
func(foobar)
macr(foobar)

调用func()和调用macr()都输出相同的内容:

image-20220112163927519

除了命名参数之外,函数和宏还提供了一组自动定义的变量,允许处理除了或代替命名参数的参数:

  • ARGC

这将被设置为传递给函数的参数总数。它计算给出的命名参数加上任何附加的未命名参数。

  • ARGV

这是一个列表变量,包含传递给函数的每个参数,包括命名参数和任何附加的未命名参数。

  • ARGN

像ARGV一样,除了它只包含命名参数之外的参数(即可选的,未命名参数)。

除了上面提到的,每个单独的参数都可以以arg#的形式引用,其中#是参数的编号(例如ARG1, ARG2,等等)。这包括了命名参数,所以第一个命名参数也可以通过ARG1引用,等等。

使用ARG…变量的典型情况包括支持可选参数和实现一个命令,该命令可以接受任意数量的待处理项。考虑一个函数,它定义了一个可执行目标,将这个目标链接到某个库,并为它定义了一个测试用例。在编写测试用例时,经常会遇到这样的函数(第24章,测试)。这个函数不是为每个测试用例重复步骤,而是允许定义一次步骤,然后每个测试用例变成一个简单的单行定义。

# Use a named argument for the target and treat all remaining
# (unnamed) arguments as the source files for the test case
function(add_mytest targetName)
  add_executable(${targetName} ${ARGN})
  target_link_libraries(${targetName} PRIVATE foobar)
  add_test(NAME ${targetName}
  COMMAND ${targetName}
  )
endfunction()
# Define some test cases using the above function
add_mytest(smallTest small.cpp)
add_mytest(bigTest big.cpp algo.cpp net.cpp)

上面的例子特别展示了ARGN变量的有用性。它允许函数或宏接受数量可变的参数,但仍然指定一组必须提供的命名参数。然而,有一种特定的情况需要注意,这种情况可能会导致意想不到的行为。因为宏把它们的参数当作字符串替换而不是变量,如果他们在需要变量名的地方使用ARGN,引用的变量将在宏被调用的范围内,而不是宏自己的参数中的ARGN。下面的例子说明了这种情况:

# WARNING: This macro is misleading
macro(dangerous)
  # Which ARGN?
  foreach(arg IN LISTS ARGN)
  message("Argument: ${arg}")
  endforeach()
endmacro()
function(func)
  dangerous(1 2)
endfunction()
func(3)

image-20220112195858082

在foreach()中使用LISTS关键字时,必须给出变量名,但为宏提供的ARGN不是变量名。当宏从另一个函数内部调用时,该宏最终会使用该封闭函数的ARGN变量,而不是宏本身的ARGN变量。当将宏体的内容直接粘贴到调用它的函数中时(这就是CMake会对它做的事情),情况就变得很清楚了:

function(func)
  # Now it is clear, ARGN here will use the arguments from func
  foreach(arg IN LISTS ARGN)
  	message("Argument: ${arg}")
  endforeach()
endfunction()

在这种情况下,考虑将宏改为函数,或者如果它必须保持为宏,则避免将实参视为变量。在上面的例子中,dangerous()的实现可以改为使用foreach(arg IN ITEMS ${ARGN})。

7.3 关键字参数

上一节说明了如何使用ARG…变量来处理参数的变量集。对于只需要一组变量或可选参数的简单情况,该功能已经足够了,但如果必须支持多个可选或可选参数集,则处理将变得非常繁琐。此外,与许多CMake自己的内置命令相比,上面描述的基本参数处理是相当严格的,这些内置命令支持基于关键字的参数和灵活的参数排序。考虑target_link_libraries()命令:

target_link_libraries(targetName
  <PRIVATE|PUBLIC|INTERFACE> item1 [item2 ...]
  [<PRIVATE|PUBLIC|INTERFACE> item3 [item4 ...]]
  ...
)

targetName必须作为第一个参数,但在此之后,调用者可以以任何顺序提供任意数量的PRIVATE、PUBLIC或INTERFACE节,每个节允许包含任意数量的项。用户定义的函数和宏可以通过使用cmake_parse_arguments()命令来支持相同的灵活性:

include(CMakeParseArguments) # Needed only for CMake 3.4 and earlier
cmake_parse_arguments(prefix
  noValueKeywords
  singleValueKeywords
  multiValueKeywords
  argsToParse)

cmake_parse_arguments()命令过去是由CMakeParseArguments模块提供的,但它成为了CMake 3.5中的内置命令。include(CMakeParseArguments)行在CMake 3.5及以后的版本中不起任何作用,而在CMake的早期版本中,它将定义cmake_parse_arguments()命令。上面的形式确保命令是可用的,无论使用的是什么CMake版本。

cmake_parse_arguments()接受argsToParse参数提供的参数,并根据指定的关键字集处理它们。通常,argsToParse被赋值为${ARGN},这是传递给封闭函数或宏的一组未命名参数。每个关键字参数都是该函数或宏支持的关键字名称的列表,因此它们都应该用引号括起来,以确保它们被正确解析。

​noValueKeywords定义独立的关键字参数,其作用类似布尔开关。关键字的出现说明了一件事,它的缺席说明了另一件事。使用singleValueKeywords时,每个关键字后面都需要一个额外参数,而multiValueKeywords在关键字后面需要零个或多个额外参数。虽然不是必需的,但流行的约定是关键字都是大写的,如果需要的话,用下划线分隔单词。但是请注意,关键字不应该太长,否则使用起来会很麻烦。

​当cmake_parse_arguments()返回时,对于每个关键字,对应的变量将可用,其名称由指定的前缀、下划线和关键字名称组成。例如,如果前缀为ARG,则与关键字FOO对应的变量将是ARG_FOO。如果argsToParse中没有出现特定的关键字,则其对应的变量将为空。下面的例子最好地说明了这三种不同的关键字类型是如何定义和处理的:

function(func)
  # Define the supported set of keywords
  set(prefix ARG)
  set(noValues ENABLE_NET COOL_STUFF)
  set(singleValues TARGET)
  set(multiValues SOURCES IMAGES)
  # Process the arguments passed in
  include(CMakeParseArguments)
  cmake_parse_arguments(${prefix}
  "${noValues}"
  "${singleValues}"
  "${multiValues}"
  ${ARGN})
  # Log details for each supported keyword
  message("Option summary:")
  foreach(arg IN LISTS noValues)
  	if(${${prefix}_${arg}})
  		message(" ${arg} enabled")
  	else()
  		message(" ${arg} disabled")
  	endif()
  endforeach()
  foreach(arg IN LISTS singleValues multiValues)
  	# Single argument values will print as a simple string
  	# Multiple argument values will print as a list
  	message(" ${arg} = ${${prefix}_${arg}}")
  endforeach()
endfunction()
# Examples of calling with different combinations
# of keyword arguments
func(SOURCES foo.cpp bar.cpp TARGET myApp ENABLE_NET)
func(COOL_STUFF TARGET dummy IMAGES here.png there.png gone.png)

相应的输出如下所示:

Option summary:
  ENABLE_NET enabled
  COOL_STUFF disabled
  TARGET = myApp
  SOURCES = foo.cpp;bar.cpp
  IMAGES =
Option summary:
  ENABLE_NET disabled
  COOL_STUFF enabled
  TARGET = dummy
  SOURCES =
  IMAGES = here.png;there.png;gone.png

​与使用命名参数和/或ARG…变量的基本参数处理相比,cmake_parse_arguments()的优点很多。

  • 由于是基于关键字的,调用站点提高了可读性,因为这些参数基本上是自文档化的。阅读调用站点的其他开发人员通常不需要查看函数实现或其文档来理解每个参数的含义。
  • 调用者可以选择参数给出的顺序。
  • 调用者可以简单地忽略那些不需要提供的参数。
  • 由于每个支持的关键字都必须传递给cmake_parse_arguments(),并且它通常在函数的顶部附近被调用,所以通常很清楚函数支持哪些参数。
  • 由于基于关键字的参数的解析是由cmake_parse_arguments()命令处理的,而不是由一个特别的、手动编码的解析器处理的,因此参数解析的bug实际上是消除了的。

7.4 范围

函数和宏之间的一个根本区别是,函数引入了一个新的变量范围,而宏没有。在函数内部定义或修改的变量对函数外部的同名变量没有影响。就变量而言,函数本质上是它自己的自包含沙箱,不像宏与它们的调用者共享相同的变量作用域。请注意,函数不会引入新的政策范围。

与C/C++中的对应函数不同,CMake函数和宏不支持直接返回值。此外,由于函数引入了它们自己的变量作用域,似乎没有简单的方法将信息传递回调用者,但事实并非如此。与add_subdirectory()方法相同,“Scope”也可以用于函数。set()命令的PARENT_SCOPE关键字可用于修改调用者范围内的变量,而不是函数内的本地变量。虽然这与从函数返回值不同,但它确实允许将一个值(或多个值)传递回调用者。

一种常见的方法是允许变量名作为函数参数传入,以便调用者仍然控制设置函数结果的变量名。这是cmake_parse_arguments()使用的方法,它的前缀参数决定它在调用者范围内设置的所有变量名的前缀。下面的例子演示了如何实现这种技术:

function(func resultVar1 resultVar2)
  set(${resultVar1} "First result" PARENT_SCOPE)
  set(${resultVar2} "Second result" PARENT_SCOPE)
endfunction()
func(myVar otherVar)
message("myVar: ${myVar}")
message("otherVar: ${otherVar}")

image-20220112203928376

另一种方法是让函数记录它设置的变量,而不是允许调用者指定变量名。这是不太理想的,因为它降低了函数的灵活性,并增加了变量名冲突的机会。在可能的情况下,最好使用上面的方法来让调用者控制正在设置或修改的变量名。

​宏可以像处理函数一样处理,通过将变量作为参数传入来指定要设置的变量名。唯一的区别是,不应该在宏中使用PARENT_SCOPE关键字,因为它已经修改了调用者作用域中的变量。事实上,使用宏而不是函数的唯一原因是需要在调用作用域中设置许多变量。宏在每次set()调用时都会影响调用作用域,而函数只有在将PARENT_SCOPE显式地指定给set()时才会影响调用作用域。

return()语句被讨论为在文件或函数中提前结束处理的一种方法。如上所述,return()不返回值,它只将处理返回到父范围。如果在函数中调用了return(),则处理立即返回给调用者,即跳过函数的其余部分。另一方面,宏中的return()的行为是非常不同的。因为宏不引入新的作用域,return()语句的行为取决于宏被调用的位置。回想一下,宏有效地将它的命令粘贴到调用站点。在这种情况下,宏的任何return()语句实际上都将从宏的作用域返回,而不是从宏本身返回。考虑以下例子:

macro(inner)
  message("From inner")
  return() # Usually dangerous within a macro
  message("Never printed")
endmacro()
function(outer)
  message("From outer before calling inner")
  inner()
  message("Also never printed")
endfunction()
outer()

image-20220112204248926

要强调为什么函数体中的第二条消息永远不会打印出来,请将宏体的内容粘贴到调用它的地方:

function(outer)
  message("From outer before calling inner")
  # === Pasted macro body ===
  message("From inner")
  return()
  message("Never printed")
  # === End of macro body ===
  message("Also never printed")
endfunction()
outer()

现在就更清楚了为什么return()语句导致处理离开函数,即使它最初是在宏内部调用的。这突出了在宏中使用return()的危险。因为宏不创建它们自己的作用域,return()语句的结果通常不是预期的。

7.5 重载命令

当function()或macro()被调用来定义一个新命令时,如果一个命令已经有了这个名字,没有记录的CMake行为是让旧的命令使用相同的名字,除了前面有一个下划线。无论旧名称是用于内置命令还是自定义函数或宏,这都适用。意识到这种行为的开发人员有时会试图利用它来创建一个现有命令的wrapper,如下所示:

function(someFunc)
  # Do something...
endfunction()
# Later in the project...
function(someFunc)
  if(...)
  # Override the behavior with something else...
  else()
  	# WARNING: Intended to call the original command, but it is not safe
  	_someFunc()
  endif()
endfunction()

如果命令只像这样被重写一次,那么它看起来是有效的,但是如果它再次被重写,那么原始的命令将不再可访问。前置一个下划线以“保存”前面的命令只适用于当前名称,它不会递归地应用于前面的所有重写。这有可能导致无限递归,如下面的例子所示:

function(printme)
  message("Hello from first")
endfunction()
function(printme)
  message("Hello from second")
  _printme()
endfunction()
function(printme)
  message("Hello from third")
  _printme()
endfunction()
printme()

人们会天真地期望输出如下:

Hello from third
Hello from second
Hello from first

但是,第一个实现永远不会被调用,因为第二个实现最终会在一个无限循环中调用自己。当CMake处理上述内容时,会发生以下情况:

  1. printme的第一个实现被创建并以命令的形式提供
  2. 这里遇到了printme的第二个实现。CMake通过这个名称找到一个现有的命令,所以它定义了名称_printme来指向旧的命令,并设置printme指向新的定义。
  3. 本文还介绍了printme的第三个实现。同样,CMake通过这个名称找到一个现有的命令,因此它重新定义了名称_printme来指向旧的命令(这是第二个实现),并设置printme指向新的定义。

实际输出:

image-20220112205200452

当printme()被调用时,执行进入第三个实现,它调用_printme()。这将进入第二个实现,它也调用_printme(),但_printme()再次指向第二个实现,并得到无限递归的结果。执行永远不会到达第一个实现。

一般来说,覆盖函数或宏是可以的,只要它不尝试调用前面的实现,就像上面讨论的那样。项目应该简单地假设新实现取代旧实现,旧实现被认为不再可用。
相关代码:https://gitee.com/jiangli01/cmake-learning
更多请关注微信公众号【Hope Hut】:
在这里插入图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

CMake(七):函数和宏 的相关文章

随机推荐

  • 三次握手,四次挥手白话文

    三次握手和四次挥手是TCP协议中用于建立和断开连接的过程 三次握手 Three way Handshake 客户端向服务器发送一个SYN 同步 包 其中包含一个随机的初始序列号 表示客户端请求建立连接 服务器收到客户端的SYN包后 向客户端
  • 2022互联网精英副业指南,看到程序员的我笑了~

    不得不说 互联网人收入高 如果你以为互联网人收入高是因为工资高 年终奖丰厚 那你就错了 其实 还有一个原因是他们搞起了副业 副业千万条 闲鱼第一条 万万没想到的是 互联网人在闲鱼上赚钱也与众不同 甚至都一个比一个拼 https mmbiz
  • 【Java 集合 & 数据结构】优先队列 PriorityQueue

    优先队列 PriorityQueue 一 概述 二 结构 三 解析 1 核心属性 2 核心方法 offer 方法 入队列 poll 方法 出队列 peek 方法 队头元素 最小元 四 特点 优点 缺点 一 概述 优先队列 PriorityQ
  • Android String字符串截取方法总结

    Android String字符串截取方法总结 指定字符 截取字符串 返回字符串数组 String str abcd efg 123456 hijk 345 String strs str split 指定索引号 截取字符串 将字符串从索引
  • 服务器上创建Python虚拟环境

    应用场景 不同的项目 或者同一项目的不同版本 需要安装不同的Python解释器和依赖库 对于有python版本依赖的程序来说 为了安全可靠的管理环境 需要创建不同版本的 独立 隔离 的虚拟环境 virtualenv 是一个创建隔绝的Pyth
  • Java设计与实现“秒杀”活动之抢粽子【完整版】

    五月榴花妖艳烘 绿杨带雨垂垂重 五月新丝缠角粽 金盘送 生绡画扇盘双凤 正是浴兰时节动 正值端午佳节 实习公司也是例行放假三天以及给每一位员工发放了节日小礼品 过完端午又将迎来618活动专场 秒杀抢单活动也是此起彼伏 从而产生刺激性消费 由
  • 使用HiBurn烧录鸿蒙.bin文件到Hi3861开发板

    使用HiBurn烧录鸿蒙 bin文件到Hi3861开发板 鸿蒙官方文档的 Hi3861开发板第一个示例程序 中描述了 如何使用DevEco Device Tool工具烧录二进制文件到Hi3861开发板 本文将介绍如何使用HiBurn工具烧录
  • 网站服务器放本地还是云上,服务器放本地还是云上安全

    服务器放本地还是云上安全 内容精选 换一换 在弹性云服务器上安装完成后输入公网IP 无法连接目的虚拟机 端口无法访问工具 源端网络未连通目的端 目的端安全组未开放8084端口 目的端网络ACL禁用了8084端口 登录源端服务器后 在源端服务
  • Leetcode 计算质数 -- 埃氏筛、线性筛解析

    0 题目描述 leetcode原题链接 204 计数质数 1 埃氏筛 很直观的思路是我们枚举每个数判断其是不是质数 枚举没有考虑到数与数的关联性 因此难以再继续优化时间复杂度 介绍一个常见的算法 该算法由希腊数学家厄拉多塞 Eratosth
  • C语言/实现MD5加密

    本文详细视频讲解 已经发布到B站 https www bilibili com video BV1uy4y1p7on 更多仔细 请关注公众号 一口Linux 一 摘要算法 摘要算法又称哈希算法 它表示输入任意长度的数据 输出固定长度的数据
  • C语言头文件路径相关问题总结说明

    聊聊系统路径位置 绝对路径与相对路径 正斜杠 与 反斜杠 使用说明 by 矜辰所致 目录 前言 一 C语言中的头文件引用 二 KEIL 中的头文件路径 2 1 IncudePaths 指定的路径 绝对路径和相对路径 正斜杠 与 反斜杠 与双
  • SpringBoot Sleuth Zipkin Dubbo日志链路追踪全流程(2)

    SpringBoot SpringCloud Sleuth Zipkin Dubbo日志链路追踪全流程 看这篇文章之前 你最好看一下 之前的文章 SpringBoot SpringCloud Sleuth Zipkin Http Log4j
  • RC电路(一):微分

    1 充放电时间常数 在模拟 数字电路中 常常用到由电阻 和电容 组成的 电路 和 的取值不同 会导致输出波形和输入波形之间的关系也不同 由此也会产生不同的应用 当 时 电容电压 0 63E 当 时 电容电压 0 86E 当 时 电容电压 0
  • 通用嵌入式系统测试平台 ETest简介

    通用嵌入式系统自动化测试平台 通用嵌入式系统测试平台 Embedded System Interface Test Studio 简称 ETest 是针对嵌入式系统进行实时 闭环 非侵入式测试的自动化测试平台 适用于嵌入式系统在设计 仿真
  • 《阵列信号处理及MATLAB实现》绪论、矩阵代数相关内容总结笔记

    第一章 绪论 1 1 研究背景 1 1 1 阵列信号处理简介 将一组传感器按照一定方式布置在空间的不同位置 形成传感器阵列 用传感器阵列来接收空间信号 相当于对空间分布的场信号采样 得到信号源的空间离散观测数据 通过对阵列接受的信号进行处理
  • Lex和Yacc应用方法(一).初识Lex

    Lex和Yacc应用方法 一 初识Lex 草木瓜 20070301 Lex Lexical Analyzar 词法分析生成器 Yacc Yet Another Compiler Compiler编译器代码生成器 是Unix下十分重要的词法分
  • Feign的使用

    基于Feign远程调用 Feign说明 Feign是一个声明式的http客户端 其作用是帮助我们优雅的实现http请求的发送 官网地址 https github com OpenFeign feign Feign的使用 修改服务的pom x
  • MVC中的项目案例

    我们先一起来看看超期的效果图吧 以上就是超期的效果图 我来解析一下 超期操作的模态窗体弹出的条件与归还一样 应选择需要超期的书籍 再弹出模态窗体 模态窗体弹出 数据自动回填上去 罚款金额 超期天数 0 2 获取当前时间为罚款时间 罚款成功后
  • 解决python3在import cv2时报错问题

    在安装了ros 在import cv2时会报错 如下 import cv2 ImportError opt ros kinetic lib python2 7 dist packages cv2 so undefined symbol Py
  • CMake(七):函数和宏

    回顾到目前为止涉及的材料 CMake的语法已经开始看起来很像一门编程语言 它支持变量 if then else逻辑 循环和包含要处理的其他文件 毫无疑问 CMake还支持常用的函数和宏编程概念 就像它们在其他编程语言中的角色一样 函数和宏是