【CMake】变量作用域1——块作用域
目录
一.块作用域
1.1.什么是块作用域?
1.2.block()和endblock()
1.3.SCOPE_FOR基本使用示例
1.3.1.不指定SCOPE_FOR
1.3.2.SCOPE_FOR VARIABLES
1.3.3.SCOPE_FOR POLICIES
1.4.PROPAGATE基本使用示例
1.4.1.基本使用示例
1.4.2.PROPAGATE的缺陷
1.4.3.谈谈set( ... [PARENT_SCOPE])
大家看看官网对于变量是怎么说的:cmake-language(7) — CMake 4.1.1 Documentation
我们把它翻译过来就是下面这样子:
变量是 CMake 语言中的基本存储单元。它们的值始终是字符串类型,尽管有些命令可能会将这些字符串解释为其他类型的值。
set()
和 unset()
命令显式地设置或取消设置变量,但其他命令的语义也会修改变量。变量名区分大小写,并且可以包含几乎任何文本,但我们建议仅使用字母数字字符加上 _
和 -
来命名。
变量具有动态作用域。每个变量的“设置”或“取消设置”操作都会在当前作用域中创建一个绑定:
块作用域 (Block Scope)
block()
命令可以为变量绑定创建一个新的作用域。
函数作用域 (Function Scope)
由 function()
命令创建的命令定义,在调用时会在一个新的变量绑定作用域中处理记录的命令。变量的“设置”或“取消设置”操作会绑定在此作用域中,并且对当前函数及其内部的任何嵌套调用可见,但在函数返回后不可见。
目录作用域 (Directory Scope)
源代码树中的每个目录都有其自己的变量绑定。在处理目录的 CMakeLists.txt
文件之前,CMake 会复制父目录中当前定义的所有变量绑定(如果有的话)来初始化新的目录作用域。使用 cmake -P
处理的 CMake 脚本在一个“目录”作用域中绑定变量。
不在函数调用内部的变量“设置”或“取消设置”操作会绑定到当前目录作用域。
持久化缓存 (Persistent Cache)
CMake 存储了一组独立的“缓存”变量(或“缓存条目”),它们的值在项目构建树的多次运行期间持久存在。缓存条目具有一个独立的作用域绑定,只能通过显式请求(例如 set()
和 unset()
命令的 CACHE
选项)进行修改。
在计算变量引用时,CMake 首先搜索函数调用堆栈(如果有)中的绑定,然后回退到当前目录作用域中的绑定(如果有)。如果找到了“设置”的绑定,则使用其值。如果找到了“取消设置”的绑定,或者没有找到任何绑定,CMake 随后会搜索缓存条目。如果找到了缓存条目,则使用其值。否则,变量引用会计算为一个空字符串。可以使用 $CACHE{VAR}
语法来直接查找缓存条目。
cmake-variables(7)
手册文档记录了由 CMake 提供的许多变量,或者在由项目代码设置时对 CMake 有意义的变量。
注意:CMake 保留以下标识符:
-
以
CMAKE_
(大写、小写或混合大小写)开头的标识符,或 -
以
_CMAKE_
(大写、小写或混合大小写)开头的标识符,或 -
以
_
开头后跟任何 CMake 命令名称的标识符。
现在我们就来讲讲变量的基本使用方法
1. 设置变量:
set()
set()
命令是定义和修改变量的主要方式。
# 设置一个普通变量
set(MY_VARIABLE "Hello, World!")# 设置一个列表(变量值中包含分号;分隔的字符串,或被理解为列表)
set(MY_LIST "a" "b" "c") # MY_LIST 的值为 "a;b;c"
set(MY_LIST a b c) # 等效写法
2. 取消设置变量:
unset()
从当前作用域中移除一个变量。
unset(MY_VARIABLE)
3. 引用变量:
${}
要获取一个变量的值,使用 ${变量名}
语法进行解引用。
set(SOURCES main.cpp helper.cpp)
message("Sources are: ${SOURCES}") # 输出:Sources are: main.cpp;helper.cpp# 在命令参数中使用
add_executable(MyApp ${SOURCES})
重要提示:在 if()
条件语句中,变量名可以直接使用,而不需要 ${}
。
if (MY_VARIABLE) # 正确:直接使用变量名
if (${MY_VARIABLE}) # 错误(在大多数情况下):这会将变量的值作为变量名再次解析,容易导致意外行为。
变量的使用方法大概也就上面这3种,那么接下来我们将讲讲变量作用域里面的块作用域,至于其他作用域的话,我们到后续的文章再进行讲解
一.块作用域
我们来深入探讨一下 CMake 中的“块作用域”(Block Scope)。
这是一个相对高级且强大的特性,在 CMake 3.25 及更高版本中引入,用于解决传统 function()
和 macro()
在变量处理上的一些局限和副作用。
注意:在使用块作用域之前必须检查一下自己的主机上的cmake的版本号必须大于或等于CMake 3.25
1.1.什么是块作用域?
块作用域是由 block()
和 endblock()
命令对显式创建的一个新的、临时的变量作用域。在这两个命令之间定义的任何变量,其生命周期和可见性都被限制在这个块内,类似于许多编程语言(如C、C++、Java)中由 { }
所创建的作用域。
基本语法:
block()# 在此块内设置的变量是局部的set(local_var "I'm only visible inside this block")message("Inside block: ${local_var}") # 可以正常打印
endblock()message("Outside block: ${local_var}") # 错误!local_var 在这里未定义
我们可以来看看block()和endblock()
1.2.block()和endblock()
block()
大家可以去官网进行查询:block — CMake 4.1.1 Documentation
block
在 3.25 版本中加入。
使用专用的变量和/或策略作用域来评估一组命令。
语法:
block([SCOPE_FOR [POLICIES] [VARIABLES]] [PROPAGATE <var-name>...])<commands>
endblock()
描述:
位于 block()
和与之匹配的 endblock()
之间的所有命令会被记录下来但不会立即执行。一旦 endblock()
被求值,记录的命令列表会在所请求的作用域内被调用,然后由 block()
命令创建的作用域会被移除。
SCOPE_FOR
指定必须创建哪些作用域。
-
POLICIES: 创建一个新的策略作用域。这等价于先执行
cmake_policy(PUSH)
,并在离开块作用域时自动执行cmake_policy(POP)
。 -
VARIABLES: 创建一个新的变量作用域。
如果未指定 SCOPE_FOR
,则等价于:
默认情况下:
block()
等价于
block(SCOPE_FOR VARIABLES POLICIES)
也就是说,同时新建变量作用域和策略作用域。
PROPAGATE
当由 block()
命令创建了一个变量作用域时,此选项会在父作用域中设置或取消设置指定的变量。这等价于使用 set(PARENT_SCOPE)
或 unset(PARENT_SCOPE)
命令。
示例:
set(var1 "INIT1")
set(var2 "INIT2")
set(var3 "INIT3")block(PROPAGATE var1 var2)set(var1 "VALUE1") # 设置当前作用域的 var1,PROPAGATE 会将其传播到父作用域unset(var2) # 取消设置当前作用域的 var2,PROPAGATE 会将其传播到父作用域set(var3 "VALUE3") # 仅设置当前作用域的 var3,不会被传播(因为没有在 PROPAGATE 列表中)
endblock()# 现在 var1 的值为 "VALUE1",var2 未被设置,var3 的值仍为初始的 "INIT3"
此选项仅在创建了变量作用域时(即指定SCOPE_FOR为VARIABLES或者不指定SCOPE_FOR)才被允许使用。在其他情况下(即未创建变量作用域时使用 PROPAGATE
)会引发错误。
当 block()
位于 foreach()
或 while()
循环内部时,可以在 block()
内部使用 break()
和 continue()
命令。
示例:
while(TRUE)block()...# break() 命令将终止外部的 while() 循环break()endblock()
endwhile()
endblock()
`endblock()`
*在 3.25 版本中加入*
结束由 `block()` 命令开启的命令列表,并移除由 `block()` 命令创建的作用域。
1.3.SCOPE_FOR基本使用示例
1.3.1.不指定SCOPE_FOR
案例1——外部访问块作用域内部的变量
项目结构
test/
└── CMakeLists.txt
CMakeLists.txt
cmake_minimum_required(VERSION 3.25)
project(BlockScopeDemo)block()# 在此块内设置的变量是局部的set(local_var "I'm only visible inside this block")message("Inside block: ${local_var}") # 可以正常打印
endblock()message("Outside block: ${local_var}") # 错误!local_var 在这里未定义
其实我们可以执行下面这个来一键搭建出这个目录结构
mkdir -p test && cat > test/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.25)
project(BlockScopeDemo)block()# 在此块内设置的变量是局部的set(local_var "I'm only visible inside this block")message("Inside block: ${local_var}") # 可以正常打印
endblock()message("Outside block: ${local_var}") # 错误!local_var 在这里未定义
EOF
接下来我们就来构建一下项目
mkdir build && cd build && cmake ..
我们发现块作用域确实访问不到块作用域内部的变量。
案例2——块作用域内部访问外部作用域变量
项目结构
test/
└── CMakeLists.txt
CMakeLists.txt
cmake_minimum_required(VERSION 3.25)
project(BlockScopeDemo)set(out_var "外部变量")
message("外部的out_var: ${out_var}")block()set(out_var "内部变量")message("块作用域内的out_var: ${out_var}")
endblock()message("外部的out_var: ${out_var}")
其实我们可以执行下面这个来一键搭建出这个目录结构
mkdir -p test && cat > test/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.25)
project(BlockScopeDemo)set(out_var "外部变量")
message("外部的out_var: ${out_var}")
block()set(out_var "内部变量")message("块作用域内的out_var: ${out_var}")
endblock()
message("外部的out_var: ${out_var}")
EOF
接下来我们就来构建一下项目
mkdir build && cd build && cmake ..
我们发现
-
在 block 内修改变量不会影响外部同名变量。
-
一旦 block 结束,block 内的定义和修改全部消失。
案例3——使用block()
事实上呢!
如果未指定 SCOPE_FOR
,则默认情况下:
block()
等价于
block(SCOPE_FOR VARIABLES POLICIES)
也就是说,同时新建变量作用域和策略作用域。
我们可以看看策略这里是不是这样子
项目结构
test/
└── CMakeLists.txt
CMakeLists.txt
cmake_minimum_required(VERSION 2.8)
project(BlockPolicyDemo)# 获取 CMP0048 的状态
cmake_policy(GET CMP0048 state)
message("初始 CMP0048 策略状态: ${state}")# --- block 开始 ---
block()cmake_policy(GET CMP0048 state)message("块作用域内修改前 CMP0048 策略状态: ${state}")# 在 block 内修改策略cmake_policy(SET CMP0048 NEW)cmake_policy(GET CMP0048 state)message("块作用域内修改后 CMP0048 策略状态: ${state}")
endblock()
# --- block 结束 ---# block 结束后,策略状态会恢复
cmake_policy(GET CMP0048 state)
message("离开 block 后 CMP0048 策略状态: ${state}")
其实我们可以执行下面这个来一键搭建出这个目录结构
mkdir -p test && cat > test/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 2.8)
project(BlockPolicyDemo)# 获取 CMP0048 的状态
cmake_policy(GET CMP0048 state)
message("初始 CMP0048 策略状态: ${state}")# --- block 开始 ---
block()cmake_policy(GET CMP0048 state)message("块作用域内修改前 CMP0048 策略状态: ${state}")# 在 block 内修改策略cmake_policy(SET CMP0048 NEW)cmake_policy(GET CMP0048 state)message("块作用域内修改后 CMP0048 策略状态: ${state}")
endblock()
# --- block 结束 ---# block 结束后,策略状态会恢复
cmake_policy(GET CMP0048 state)
message("离开 block 后 CMP0048 策略状态: ${state}")
EOF
接下来我们就来构建一下项目
mkdir build && cd build && cmake ..
我们发现在块作用域内对策略进行修改,不会影响到外部的策略。同时块作用域内部可以访问到外部作用域的状态。
1.3.2.SCOPE_FOR VARIABLES
案例1——只使用block(SCOPE_FOR VARIABLES)
项目结构
test/
└── CMakeLists.txt
CMakeLists.txt
cmake_minimum_required(VERSION 3.19)
project(BlockExample)set(MY_VAR "outer")message(STATUS "进入 block 之前: MY_VAR='${MY_VAR}'")block(SCOPE_FOR VARIABLES)message(STATUS "刚进入 block 时: MY_VAR='${MY_VAR}'")set(MY_VAR "inner")message(STATUS "在 block 内部修改MY_VAR之后: MY_VAR='${MY_VAR}'")
endblock()message(STATUS "离开 block 之后: MY_VAR='${MY_VAR}'")
其实我们可以执行下面这个来一键搭建出这个目录结构
mkdir -p test && cat > test/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.19)
project(BlockExample)set(MY_VAR "outer")message(STATUS "进入 block 之前: MY_VAR='${MY_VAR}'")block(SCOPE_FOR VARIABLES)message(STATUS "刚进入 block 时: MY_VAR='${MY_VAR}'")set(MY_VAR "inner")message(STATUS "在 block 内部修改MY_VAR之后: MY_VAR='${MY_VAR}'")
endblock()message(STATUS "离开 block 之后: MY_VAR='${MY_VAR}'")
EOF
接下来我们就来构建一下项目
mkdir build && cd build && cmake ..
我们发现在只使用block(SCOPE_FOR VARIABLES)时,在块作用域内对变量进行修改,不会影响到块作用域外的!!
案例2——只使用block(SCOPE_FOR POLICIES)
项目结构
test/
└── CMakeLists.txt
CMakeLists.txt
cmake_minimum_required(VERSION 3.19)
project(BlockExample)set(MY_VAR "outer")message(STATUS "进入 block 之前: MY_VAR='${MY_VAR}'")block(SCOPE_FOR POLICIES)message(STATUS "刚进入 block 时: MY_VAR='${MY_VAR}'")set(MY_VAR "inner")message(STATUS "在 block 内部修改MY_VAR之后: MY_VAR='${MY_VAR}'")
endblock()message(STATUS "离开 block 之后: MY_VAR='${MY_VAR}'")
其实我们可以执行下面这个来一键搭建出这个目录结构
mkdir -p test && cat > test/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.19)
project(BlockExample)set(MY_VAR "outer")message(STATUS "进入 block 之前: MY_VAR='${MY_VAR}'")block(SCOPE_FOR POLICIES)message(STATUS "刚进入 block 时: MY_VAR='${MY_VAR}'")set(MY_VAR "inner")message(STATUS "在 block 内部修改MY_VAR之后: MY_VAR='${MY_VAR}'")
endblock()message(STATUS "离开 block 之后: MY_VAR='${MY_VAR}'")
EOF
接下来我们就来构建一下项目
mkdir build && cd build && cmake ..
我们发现在只使用block(SCOPE_FOR POLICIES)时,在块作用域内对变量进行修改,不会影响到块作用域外的!!
1.3.3.SCOPE_FOR POLICIES
案例1——只使用block(SCOPE_FOR POLICIES)
项目结构
test/
└── CMakeLists.txt
CMakeLists.txt
cmake_minimum_required(VERSION 2.8)
project(BlockPolicyDemo)# 获取 CMP0048 的状态
cmake_policy(GET CMP0048 state)
message("初始 CMP0048 策略状态: ${state}")# --- block 开始 ---
block(SCOPE_FOR POLICIES)cmake_policy(GET CMP0048 state)message("块作用域内修改前 CMP0048 策略状态: ${state}")# 在 block 内修改策略cmake_policy(SET CMP0048 NEW)cmake_policy(GET CMP0048 state)message("块作用域内修改后 CMP0048 策略状态: ${state}")
endblock()
# --- block 结束 ---# block 结束后,策略状态会恢复
cmake_policy(GET CMP0048 state)
message("离开 block 后 CMP0048 策略状态: ${state}")
其实我们可以执行下面这个来一键搭建出这个目录结构
mkdir -p test && cat > test/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 2.8)
project(BlockPolicyDemo)# 获取 CMP0048 的状态
cmake_policy(GET CMP0048 state)
message("初始 CMP0048 策略状态: ${state}")# --- block 开始 ---
block(SCOPE_FOR POLICIES)cmake_policy(GET CMP0048 state)message("块作用域内修改前 CMP0048 策略状态: ${state}")# 在 block 内修改策略cmake_policy(SET CMP0048 NEW)cmake_policy(GET CMP0048 state)message("块作用域内修改后 CMP0048 策略状态: ${state}")
endblock()
# --- block 结束 ---# block 结束后,策略状态会恢复
cmake_policy(GET CMP0048 state)
message("离开 block 后 CMP0048 策略状态: ${state}")
EOF
接下来我们就来构建一下项目
mkdir build && cd build && cmake ..
我们发现
-
CMake中的
block()
和endblock()
命令可以用来创建一个作用域,在这个作用域内对策略的修改不会影响到外部作用域。这有助于在局部改变策略而不影响全局设置。
案例2——只使用block(SCOPE_FOR VARIABLES)
项目结构
test/
└── CMakeLists.txt
CMakeLists.txt
cmake_minimum_required(VERSION 2.8)
project(BlockPolicyDemo)# 获取 CMP0048 的状态
cmake_policy(GET CMP0048 state)
message("初始 CMP0048 策略状态: ${state}")# --- block 开始 ---
block(SCOPE_FOR VARIABLES)cmake_policy(GET CMP0048 state)message("块作用域内修改前 CMP0048 策略状态: ${state}")# 在 block 内修改策略cmake_policy(SET CMP0048 NEW)cmake_policy(GET CMP0048 state)message("块作用域内修改后 CMP0048 策略状态: ${state}")
endblock()
# --- block 结束 ---# block 结束后,策略状态会恢复
cmake_policy(GET CMP0048 state)
message("离开 block 后 CMP0048 策略状态: ${state}")
其实我们可以执行下面这个来一键搭建出这个目录结构
mkdir -p test && cat > test/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 2.8)
project(BlockPolicyDemo)# 获取 CMP0048 的状态
cmake_policy(GET CMP0048 state)
message("初始 CMP0048 策略状态: ${state}")# --- block 开始 ---
block(SCOPE_FOR VARIABLES)cmake_policy(GET CMP0048 state)message("块作用域内修改前 CMP0048 策略状态: ${state}")# 在 block 内修改策略cmake_policy(SET CMP0048 NEW)cmake_policy(GET CMP0048 state)message("块作用域内修改后 CMP0048 策略状态: ${state}")
endblock()
# --- block 结束 ---# block 结束后,策略状态会改变
cmake_policy(GET CMP0048 state)
message("离开 block 后 CMP0048 策略状态: ${state}")
EOF
接下来我们就来构建一下项目
mkdir build && cd build && cmake ..
我们发现在块作用域对策略进行修改,居然影响到了外部作用域的策略!!!这就是不加block(SCOPE_FOR POLICIES)的后果。
案例4——cmake_policy(PUSH) 和 cmake_policy(POP)
事实上呢,block()……endblock()其实本质上也是执行了cmake_policy(PUSH) 和 cmake_policy(POP)
- PUSH:将当前所有策略的状态压入栈中保存起来。
- POP:将栈顶的策略状态弹出,并恢复所有策略到那个状态。
我们也可以对比一下
项目结构
test/
└── CMakeLists.txt
CMakeLists.txt
cmake_minimum_required(VERSION 2.8)
project(BlockPolicyDemo)# 获取 CMP0048 的状态
cmake_policy(GET CMP0048 state)
message("初始 CMP0048 策略状态: ${state}")cmake_policy(PUSH)cmake_policy(GET CMP0048 state)message("块作用域内修改前 CMP0048 策略状态: ${state}")# 在 block 内修改策略cmake_policy(SET CMP0048 NEW)cmake_policy(GET CMP0048 state)message("块作用域内修改后 CMP0048 策略状态: ${state}")
cmake_policy(POP)# block 结束后,策略状态会恢复
cmake_policy(GET CMP0048 state)
message("离开 block 后 CMP0048 策略状态: ${state}")
其实我们可以执行下面这个来一键搭建出这个目录结构
mkdir -p test && cat > test/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 2.8)
project(BlockPolicyDemo)# 获取 CMP0048 的状态
cmake_policy(GET CMP0048 state)
message("初始 CMP0048 策略状态: ${state}")cmake_policy(PUSH)cmake_policy(GET CMP0048 state)message("块作用域内修改前 CMP0048 策略状态: ${state}")# 在 block 内修改策略cmake_policy(SET CMP0048 NEW)cmake_policy(GET CMP0048 state)message("块作用域内修改后 CMP0048 策略状态: ${state}")
cmake_policy(POP)# block 结束后,策略状态会恢复
cmake_policy(GET CMP0048 state)
message("离开 block 后 CMP0048 策略状态: ${state}")
EOF
接下来我们就来构建一下项目
mkdir build && cd build && cmake ..
我们发现和上面案例1,2的情况是一模一样的!!这就更加验证了这个事实
1.4.PROPAGATE基本使用示例
1.4.1.基本使用示例
案例1——不使用PROPAGATE
项目结构
test/
└── CMakeLists.txt
CMakeLists.txt
cmake_minimum_required(VERSION 3.19)
project(BlockNoPropagateDemo)set(VAR1 "outer1")
set(VAR2 "outer2")message("进入 block 之前: VAR1='${VAR1}', VAR2='${VAR2}'")block() # 这里不写 PROPAGATEmessage("刚进入 block 时: VAR1='${VAR1}', VAR2='${VAR2}'")set(VAR1 "inner1") # 修改 VAR1unset(VAR2) # 删除 VAR2message("在 block 内部: VAR1='${VAR1}', VAR2='${VAR2}'")
endblock()message("离开 block 之后: VAR1='${VAR1}', VAR2='${VAR2}'")
其实我们可以执行下面这个来一键搭建出这个目录结构
mkdir -p test && cat > test/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.19)
project(BlockNoPropagateDemo)set(VAR1 "outer1")
set(VAR2 "outer2")message("进入 block 之前: VAR1='${VAR1}', VAR2='${VAR2}'")block() # 这里不写 PROPAGATEmessage("刚进入 block 时: VAR1='${VAR1}', VAR2='${VAR2}'")set(VAR1 "inner1") # 修改 VAR1unset(VAR2) # 删除 VAR2message("在 block 内部: VAR1='${VAR1}', VAR2='${VAR2}'")
endblock()message("离开 block 之后: VAR1='${VAR1}', VAR2='${VAR2}'")
EOF
接下来我们就来构建一下项目
mkdir build && cd build && cmake ..
在 CMake 中,block()
会创建一个新的变量作用域。
在该作用域内可以对外部作用域的同名变量进行修改,但这些修改默认仅在块作用域内部有效。
当退出块作用域时,变量会恢复为进入块之前的状态。
因此,除非显式使用 PROPAGATE
选项,否则在块作用域中对外部同名变量的修改不会影响父作用域。
案例2——使用PROPAGATE
项目结构
test/
└── CMakeLists.txt
CMakeLists.txt
cmake_minimum_required(VERSION 3.19)
project(BlockPropagateSetUnsetDemo)# 初始化两个变量
set(VAR1 "outer1")
set(VAR2 "outer2")message("进入 block 之前: VAR1='${VAR1}', VAR2='${VAR2}'")block(PROPAGATE VAR1 VAR2)message("刚进入 block 时: VAR1='${VAR1}', VAR2='${VAR2}'")# 修改 VAR1set(VAR1 "inner1")# 删除 VAR2unset(VAR2)message("在 block 内部: VAR1='${VAR1}', VAR2='${VAR2}'")
endblock()# 直接打印,不做条件判断
message("离开 block 之后: VAR1='${VAR1}', VAR2='${VAR2}'")
其实我们可以执行下面这个来一键搭建出这个目录结构
mkdir -p test && cat > test/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.19)
project(BlockPropagateSetUnsetDemo)# 初始化两个变量
set(VAR1 "outer1")
set(VAR2 "outer2")message("进入 block 之前: VAR1='${VAR1}', VAR2='${VAR2}'")block(PROPAGATE VAR1 VAR2)message("刚进入 block 时: VAR1='${VAR1}', VAR2='${VAR2}'")# 修改 VAR1set(VAR1 "inner1")# 删除 VAR2unset(VAR2)message("在 block 内部: VAR1='${VAR1}', VAR2='${VAR2}'")
endblock()# 直接打印,不做条件判断
message("离开 block 之后: VAR1='${VAR1}', VAR2='${VAR2}'")
EOF
接下来我们就来构建一下项目
mkdir build && cd build && cmake ..
我们发现在块作用域中对外部作用域的同名变量进行带PARENT_SCOPE参数修改之后,竟然真的影响到了外部作用域。
示例3
block(PROPAGATE VAR1 VAR2)
里列出的变量var1,var2 不一定要在外部作用域里预先定义。它的意思是:
-
在 block 内部,这些变量如果被
set()
或unset()
,那么在退出 block 时,它们的结果会同步到父作用域。 -
如果父作用域里原本没有定义,那么就会在父作用域里新建(对应
set(PARENT_SCOPE)
的效果)。 -
如果父作用域里有定义,那么就会覆盖或删除。
我们直接看例子
项目结构
test/
└── CMakeLists.txt
CMakeLists.txt
cmake_minimum_required(VERSION 3.19)
project(BlockPropagateSetUnsetDemo)#没有定义这2个变量
message("进入 block 之前: VAR1='${VAR1}', VAR2='${VAR2}'")block(PROPAGATE VAR1 VAR2)message("刚进入 block 时: VAR1='${VAR1}', VAR2='${VAR2}'")# 修改 VAR1set(VAR1 "inner1")# 删除 VAR2unset(VAR2)message("在 block 内部: VAR1='${VAR1}', VAR2='${VAR2}'")
endblock()# 直接打印,不做条件判断
message("离开 block 之后: VAR1='${VAR1}', VAR2='${VAR2}'")
其实我们可以执行下面这个来一键搭建出这个目录结构
mkdir -p test && cat > test/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.19)
project(BlockPropagateSetUnsetDemo)#没有定义这2个变量
message("进入 block 之前: VAR1='${VAR1}', VAR2='${VAR2}'")block(PROPAGATE VAR1 VAR2)message("刚进入 block 时: VAR1='${VAR1}', VAR2='${VAR2}'")# 修改 VAR1set(VAR1 "inner1")# 删除 VAR2unset(VAR2)message("在 block 内部: VAR1='${VAR1}', VAR2='${VAR2}'")
endblock()# 直接打印,不做条件判断
message("离开 block 之后: VAR1='${VAR1}', VAR2='${VAR2}'")
EOF
接下来我们就来构建一下项目
mkdir build && cd build && cmake ..
很好的验证了我们的猜想了吧!!!
1.4.2.PROPAGATE的缺陷
在使用 block(PROPAGATE <var>...)
时,需要预先明确列出哪些变量的修改结果会在块结束后传递到外层作用域。例如:
block(PROPAGATE VAR1 VAR2)set(VAR1 "value1")set(VAR2 "value2")
endblock()
# VAR1 与 VAR2 的值在外层可见
这种方式的限制在于:必须在进入 block()
时提前知道变量名。然而在更动态的场景下,我们往往无法预先确定所有可能需要传递的变量。
为了解决这一问题,CMake 提供了 set(... PARENT_SCOPE)
和 unset(... PARENT_SCOPE)
。
它们允许在块内部(或函数、宏内部)直接将变量的修改应用到外层作用域,而无需在 block()
中显式列出。
set(VAR2 "outer2")block()# 修改并传递到外层set(VAR1 "inner1" PARENT_SCOPE)# 删除外层已存在的变量unset(VAR2 PARENT_SCOPE)
endblock()# VAR1 在外层可见,VAR2 已被清除
示例1——使用set(<variable> <value>... [PARENT_SCOPE])
项目结构
test/
└── CMakeLists.txt
CMakeLists.txt
cmake_minimum_required(VERSION 3.19)
project(BlockParentScopeDemo)# 在外部预先定义 VAR2
set(VAR2 "outer2")message("进入 block 之前: VAR1='${VAR1}', VAR2='${VAR2}'")block()message("刚进入 block 时: VAR1='${VAR1}', VAR2='${VAR2}'")# 修改 VAR1,并传递到外层set(VAR1 "inner1" PARENT_SCOPE)# 删除 VAR2(这里外层已有,所以能 unset 成功)unset(VAR2 PARENT_SCOPE)message("在 block 内部: VAR1='${VAR1}', VAR2='${VAR2}'")
endblock()# 直接打印,不做条件判断
message("离开 block 之后: VAR1='${VAR1}', VAR2='${VAR2}'")
其实我们可以执行下面这个来一键搭建出这个目录结构
mkdir -p test && cat > test/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.19)
project(BlockParentScopeDemo)# 在外部预先定义 VAR2
set(VAR2 "outer2")message("进入 block 之前: VAR1='${VAR1}', VAR2='${VAR2}'")block()message("刚进入 block 时: VAR1='${VAR1}', VAR2='${VAR2}'")# 修改 VAR1,并传递到外层set(VAR1 "inner1" PARENT_SCOPE)# 删除 VAR2(这里外层已有,所以能 unset 成功)unset(VAR2 PARENT_SCOPE)message("在 block 内部: VAR1='${VAR1}', VAR2='${VAR2}'")
endblock()# 直接打印,不做条件判断
message("离开 block 之后: VAR1='${VAR1}', VAR2='${VAR2}'")
EOF
接下来我们就来构建一下项目
mkdir build && cd build && cmake ..
我们发现在块作用域中对外部作用域的同名变量进行带PARENT_SCOPE参数修改之后,竟然真的影响到了外部作用域。
大家可能注意到了一点奇怪的地方。
明明我们是在修改变量之后进行打印的,为啥这里还是原来的状态呢?其实这里蕴含一点机制!!
我们往下面看看
第一阶段:进入
block()
—— 作用域的创建与初始化
当 CMake 解析器执行到 block()
语句时,会触发一个精密的过程:
-
作用域栈操作:
-
CMake 维护着一个“作用域栈”,用于管理变量表的层次结构。当前正在执行的作用域位于栈顶。
-
执行
block()
时,CMake 会压入一个新的栈帧。这个新栈帧自带一个全新的、完全空白的变量表(Variable Map)。
-
-
变量表的继承策略(核心):
-
这个新创建的变量表不是保持空白。CMake 会使用一种叫做“继承式复制” 的策略来初始化它。
-
源:父作用域(即调用
block()
的作用域)的变量表。 -
操作:CMake 会遍历父作用域变量表中的每一个条目(包括变量名和其对应的值),并将它们逐一复制到新的变量表中。
-
复制的范围:这包括所有普通变量 (
set()
)、列表变量 (list()
) 、以及环境变量 ($ENV{}
)。它本质上是在block()
开始的这一刻,为父作用域的整个变量环境创建了一个快照(snapshot)或副本(clone)。
-
-
执行上下文的切换:
-
初始化完成后,CMake 将后续代码的执行上下文切换到这个新创建的作用域。从此,所有对变量的操作都基于这个新的、独立的变量表。
-
重要细节与影响:
-
值传递,而非引用传递:子块获得的是父变量值的副本。如果父变量包含一个巨大的列表,那么在块入口处会产生一次内存复制。这是为作用域隔离性付出的代价。
-
一次性快照:复制只发生在
block()
入口处。此后,即使在父作用域中修改了变量的值,块内的副本也不会自动更新,因为它们已处于两个完全独立的变量表中。 -
屏蔽(Shadowing):如果块内修改了从父作用域复制来的变量,这并不会覆盖父作用域的值,而只是在子作用域的变量表中创建了一个新的条目,屏蔽了继承来的副本。父作用域的原始值在内存中依然完好无损。
第二阶段:在
block()
内部执行 —— 隔离环境中的操作
在 block()
和 endblock()
之间的所有命令,都在这个隔离的沙箱中运行:
-
变量解析(读取):
-
当遇到
${MY_VAR}
时,CMake 的解析器严格地在新创建的块级变量表中进行查找。 -
因为它拥有父作用域所有变量的副本,所以查找总能成功(除非变量是在父作用域块之后才定义的)。
-
绝不会进行动态作用域查找。它不会去父作用域寻找
MY_VAR
;它只查找自己的变量表。
-
-
变量修改(
set()
,list()
无PARENT_SCOPE
)):-
修改已有变量:
set(INHERITED_VAR "new_value")
会直接在块级变量表中找到INHERITED_VAR
并更新其值。这仅修改副本。 -
创建新变量:
set(NEW_VAR "brand_new")
会在块级变量表中创建一个全新的条目。父作用域对此一无所知。
-
-
与外部通信(使用
PARENT_SCOPE
)):-
set(OUTSIDE_VAR "new" PARENT_SCOPE)
是一个明确的越级操作指令。 -
执行此命令时,CMake 会忽略当前的块级变量表。
-
它沿着作用域栈向上回溯一层,找到父作用域的变量表,然后对该表进行修改。
-
关键后果:此操作完成后,块内的
OUTSIDE_VAR
值保持不变(仍然是当初继承来的值或之前设置的值),因为PARENT_SCOPE
指令绕过了本地操作。
-
第三阶段:退出
endblock()
—— 作用域的销毁与变量的传播
这是 block()
最强大、最独特的部分。endblock()
并非简单地销毁作用域,它提供了一个可控的接口来决定内部状态的命运。
-
默认行为:
endblock()
-
作用域销毁:CMake 将当前块级作用域从作用域栈中弹出,并随之销毁整个块级变量表。
-
内存回收:所有在该块内创建的新变量(如
NEW_VAR
)以及所有对继承变量所做的修改(如INHERITED_VAR
的新值)都将被永久丢弃。 -
效果:除了那些通过
set(... PARENT_SCOPE)
显式修改的变量,父作用域完全感知不到块内发生的任何操作。世界仿佛恢复了原样。
-
-
高级行为:
endblock(PROPAGATE <vars>...)
-
传播阶段:在销毁块级作用域之前,CMake 会执行一个额外的“传播”步骤。
-
机制:对于
PROPAGATE
关键字后列出的每一个变量名(如var1
,var2
),CMake 会:
a. 在即将销毁的块级变量表中查找该变量。
b. 如果找到,则将其当前值复制到父作用域的变量表中,覆盖父作用域中的同名变量。
c. 如果未在块级变量表中找到,则不会执行任何操作(父作用域中该变量保持不变或被创建)。 -
智能传播:这是一个单向的、一次性的值复制过程。它只在退出时发生一次。
-
设计意图:这本质上是一种批量、声明式的返回值机制。你可以将
block()
视为一个计算单元,而PROPAGATE
清单就是你指定要输出的计算结果。
-
到这里我们也就是明白了。现在我们来重新讲解一下这个机制
我们看看CMakeLists.txt
cmake_minimum_required(VERSION 3.19)
project(BlockParentScopeDemo)# 在外部预先定义 VAR2
set(VAR2 "outer2")message("进入 block 之前: VAR1='${VAR1}', VAR2='${VAR2}'")block()message("刚进入 block 时: VAR1='${VAR1}', VAR2='${VAR2}'")# 修改 VAR1,并传递到外层set(VAR1 "inner1" PARENT_SCOPE)# 删除 VAR2(这里外层已有,所以能 unset 成功)unset(VAR2 PARENT_SCOPE)message("在 block 内部: VAR1='${VAR1}', VAR2='${VAR2}'")
endblock()# 直接打印,不做条件判断
message("离开 block 之后: VAR1='${VAR1}', VAR2='${VAR2}'")
初始状态:父作用域的变量环境
在脚本最开始,我们通过 set(VAR2 "outer2")
在父作用域(即最外层的作用域)中创建了一个变量 VAR2
,并将其值设置为字符串 "outer2"
。此时,变量 VAR1
尚未被定义。因此,父作用域的变量环境中只存在一个条目:VAR2
的值是 "outer2"
。
当执行第一条 message
命令打印 VAR1
和 VAR2
的值时,CMake 会在父作用域的变量环境中进行查找。它找不到 VAR1
,所以 ${VAR1}
被解析为一个空字符串。它成功地找到了 VAR2
,并取其值 "outer2"
。这就产生了第一条输出:进入 block 之前: VAR1='', VAR2='outer2'
。
进入 block()
:作用域的创建与初始化
当执行流遇到 block()
命令时,CMake 的词法作用域机制开始启动。
首先,它会为这个块创建一个全新的、独立的变量环境(我们称之为“块级变量表”)。这个新环境初始是空的。
接着,最关键的一步发生了:CMake 将父作用域变量环境在当前时刻的完整状态复制到这个新的块级变量表中。这是一个“快照”式的复制。它不仅仅复制那些有值的变量,连“某个变量未被定义”这个状态也会被复制。
因此,在新的块级变量表中:
-
VAR2
被创建,其值是复制来的"outer2"
。 -
VAR1
的状态也被复制了,即“未被定义”。
所以,当块内的第一条 message
命令执行时,它是在这个刚初始化好的块级变量表中进行查找。查找结果和父作用域完全一致:VAR1
找不到(为空),VAR2
的值为 "outer2"
。这就产生了第二条输出:刚进入 block 时: VAR1='', VAR2='outer2'
。这条输出证实了作用域复制确实发生了。
在 block()
内部操作:隔离与向上通信
接下来,块内的代码开始修改变量。
set(VAR1 "inner1" PARENT_SCOPE)
命令中的 PARENT_SCOPE
关键字是一个明确的指令,它改变了 set
命令的行为。它告诉 CMake:“请不要修改我当前所在块级变量表中的 VAR1
,请直接去修改我父作用域的变量表”。
于是,CMake 沿着作用域链向上,找到父作用域的变量表,并在其中执行了 set(VAR1 "inner1")
的操作。至此,父作用域的变量表被更新了:VAR1
现在被定义,值为 "inner1"
。而块级变量表完全不受影响,其中的 VAR1
仍然处于“未被定义”的状态。
紧接着,unset(VAR2 PARENT_SCOPE)
命令执行。同样,PARENT_SCOPE
关键字指令CMake去父作用域操作。于是,CMake 在父作用域的变量表中删除了变量 VAR2
。此时,父作用域变量表的最新状态是:VAR1
的值为 "inner1"
,而 VAR2
不复存在。同样,这个操作丝毫不影响块级变量表,其中的 VAR2
依然安然无恙地存在着,值仍然是 "outer2"
。
现在执行块内最后一条 message
命令。这条命令仍然在块级变量表的上下文中执行。它查找 VAR1
,找不到(为空)。它查找 VAR2
,找到了,其值为 "outer2"
。因此,第三条输出是:在 block 内部: VAR1='', VAR2='outer2'
。这条输出至关重要,它强有力地证明了我们之前的分析:所有在块内的普通操作(包括读取)都只针对块级的变量副本,而 PARENT_SCOPE
操作只影响父作用域,不会“反射”回当前块的作用域。
退出 endblock()
:作用域销毁与结果定格
当执行流到达 endblock()
时,CMake 会执行收尾工作。
它首先会销毁这个块级作用域,也就是将整个块级变量表及其所有内容(包括那个值为 "outer2"
的 VAR2
副本)从内存中彻底丢弃。此后,这个隔离的沙箱环境消失,执行流回到了父作用域。
之前通过 PARENT_SCOPE
对父作用域变量表所做的所有修改都被永久地保留了下来。所以,父作用域变量表的最终状态是:
-
VAR1
存在,值为"inner1"
。 -
VAR2
不存在(已被删除)。
最终,最后一条 message
命令在父作用域中执行。它查找 VAR1
,成功找到并输出其值 "inner1"
。它查找 VAR2
,无法找到,因此 ${VAR2}
被解析为一个空字符串。这就产生了最后一条输出:离开 block 之后: VAR1='inner1', VAR2=''
。
1.4.3.谈谈set(<variable> <value>... [PARENT_SCOPE])
大家可以去官网看看:set — CMake 4.1.1 Documentation
翻译过来就是下面这样子
set(<variable> <value>... [PARENT_SCOPE])
- <variable>:要设置的变量名。
- <value>...:零个或多个值。
- [PARENT_SCOPE]:可选参数,表示把变量设置到外层作用域。
关键点在 <value>... —— 它不是只能写一个值,而是可以写多个值。
如果你写了多个 <value>
,CMake 会 把它们用分号拼接起来,存成一个列表变量。
例如:
set(MY_LIST a b c)
等价于:
set(MY_LIST "a;b;c")
也就是说,MY_LIST
实际上是一个包含 3 个元素的列表。
这个函数在当前函数或目录作用域中设置或取消设置 <variable>
:
-
如果至少提供了一个
<value>
,则将变量设置为该值(或多个值,如果提供了多个)。 -
如果没有提供任何值,则取消设置该变量。这等价于
unset(<variable>)
命令。
如果给出了 PARENT_SCOPE
选项,变量将被设置到当前作用域的上一级作用域中。每个新的 directory
或 function()
命令都会创建一个新的作用域。作用域也可以使用 block()
命令创建。set(PARENT_SCOPE)
会将变量的值设置到父目录、调用函数或外围作用域(取其适用于当前情况者)。变量值在当前作用域中的先前状态保持不变(例如,如果它之前是未定义的,它现在仍然是未定义的;如果它有一个值,它仍然是那个值)。
block(PROPAGATE)
和 return(PROPAGATE)
命令可以作为 set(PARENT_SCOPE)
和 unset(PARENT_SCOPE)
的替代方法,用于更新父作用域。
关于普通变量与缓存变量的重要注意事项:
这是 CMake 变量系统的一个关键特性,也是一个常见的困惑点。
-
变量引用解析顺序: 当 CMake 遇到一个
${VAR}
这样的变量引用时,它会按照以下顺序查找:-
第一步: 在当前作用域及封闭作用域中查找普通变量。
-
第二步: 如果找不到普通变量,然后才去查找缓存变量(即通过
-D
选项或在CMakeCache.txt
中定义的变量)。
-
-
“隐藏”效应: 这意味着,如果一个普通变量和一个缓存变量同名,普通变量会“隐藏”或“覆盖”缓存变量。
${VAR}
会返回普通变量的值。 -
取消设置的风险: 如果你用
set(VAR)
或unset(VAR)
取消设置了那个隐藏缓存变量的普通变量,缓存变量VAR
就会立刻“暴露”出来,因为现在CMake在第一步找不到普通变量,就会在第二步找到缓存变量。 -
如何安全地“清空”变量: 如果你只是想将一个普通变量的值设为空,但又不想意外暴露出一个可能存在的缓存变量,你应该使用
set(<variable> "")
。这会将普通变量的值设置为空字符串,但它仍然作为一个已定义的普通变量存在,从而继续有效地“隐藏”同名的缓存变量。