【CMake】变量作用域3——目录作用域
目录
一.目录作用域
1.1.示例1
1.2.示例2
1.3.示例3——孙子作用域会继承父作用域的变量吗?
1.4.示例4—— PARENT_SCOPE 只作用到直接父级
1.5. 特殊的“缓存变量”(Cache Variables)
一.目录作用域
其实目录作用域和add_subdirectory()有着大关系。
在 CMake 中,理解目录作用域(Directory Scope) 是掌握变量管理和项目结构的关键。其核心机制可以通过一个常见操作来观察:使用 add_subdirectory()
命令引入子目录。
每当调用 add_subdirectory()
时,CMake 都会执行一个至关重要的操作:为该子目录创建一个全新的、独立的作用域。您可以将其想象为开启了一个新的工作空间。
这个新建的子目录作用域并非凭空而来。在创建之初,它会自动地、完整地复制一份父目录作用域中的所有变量及其当前值。这种“初始继承”机制确保了子目录能够无缝地访问和利用父目录中已经定义的所有配置、路径和选项,为子目录内的构建逻辑提供了一个丰富且一致的起点。
然而,这种继承关系是严格单向且带有保护性的。一旦子目录作用域完成初始化,它与父作用域之间的变量通路就变成了“单行道”。在子目录内部,使用普通的 set()
命令对变量进行的任何修改(包括创建新变量或取消设置现有变量),其影响范围都被严格限定在该子目录自身的作用域之内。这些操作仅仅是在修改子目录所有的那份变量“副本”。
父目录作用域中的原始变量将因此保持完全不变,仿佛子目录内的所有操作都发生在一个隔离的沙箱中。这种设计提供了强大的封装性,使得每个子目录都可以独立地管理和修改其变量,而无需担心会意外地污染或破坏父目录及其他兄弟目录的配置状态。
补充说明与类比:
为了更形象地理解,您可以将其比作:
-
父目录作用域像是总公司的一套完整规章制度。
-
add_subdirectory()
像是成立了一家新的全资子公司。 -
继承意味着子公司开业时,会收到总公司规章制度的一份完整复印件,并据此初始运营。
-
隔离意味着子公司日后可以根据本地需求修改自己的那套规章制度(例如,调整假期政策),但这个修改完全不会影响总公司原有的规章制度。总公司和其他子公司看到的依然是原来的那套规则。
这种机制是 CMake 实现模块化构建的基石。它允许项目被分解为多个管理良好的子项目(库或可执行文件),每个子项目都可以在继承全局配置的同时,安全地拥有自己独立的配置空间。
set(<variable> <value>... [PARENT_SCOPE])
正是为了打破默认的隔离规则,建立一种可控的、显式的向上通信机制,CMake 为 set()
命令提供了 PARENT_SCOPE
这个关键选项。
当在子目录作用域内使用 set(VAR_NAME VALUE PARENT_SCOPE)
时,这条命令的行为发生了根本性的改变:
-
操作目标明确指向父级:该命令会绕过对当前子目录作用域内变量的任何操作,而是直接向其父目录作用域发起一个修改请求,指示其创建或更新指定的变量。这是一种明确的“向上汇报”行为。
-
不影响当前局部环境:这是一个至关重要且有时反直觉的特性。执行此命令后:
-
父目录作用域中的对应变量会被成功设置或更新。
-
然而,在当前子目录作用域内,你所访问的同名变量仍然是之前的值(可能是继承来的,也可能是之前本地设置的)。它并不会自动获得父作用域刚被设置的那个新值。
-
我们可以看看官网是怎么介绍这个命令的:set — CMake 4.1.1 Documentation
我们翻译一部分看看
set(<variable> <value>... [PARENT_SCOPE])
在当前函数或目录作用域中设置或取消设置 <variable>
:
-
如果提供了至少一个
<value>...
,则将变量设置为该值。 -
如果未提供任何值,则取消设置该变量。这等同于
unset(<variable>)
。 -
如果给出了
PARENT_SCOPE
选项,变量将在当前作用域的上一级作用域中设置。每个新的目录或function()
命令都会创建一个新的作用域。作用域也可以由block()
命令创建。set(PARENT_SCOPE)
将把变量的值设置到父目录、调用函数或外围作用域中(以当前情况的适用者为准)。变量值的先前状态在当前作用域中保持不变(例如,如果它之前未定义,则它仍然未定义;如果它有一个值,则它仍然是那个值)。
1.1.示例1
📂 目录结构
demo/
├── CMakeLists.txt
└── sub/└── CMakeLists.txt
🔹 顶层 demo/CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(DirScopeDemo)set(VAR "outer") # 定义父目录变量message(STATUS "顶层:初始 VAR='${VAR}'")add_subdirectory(sub) # 进入子目录message(STATUS "顶层:子目录结束后 VAR='${VAR}'")
🔹 子目录 demo/sub/CMakeLists.txt
message(STATUS "子目录:进入时 VAR='${VAR}'")set(VAR "inner") # 修改本地副本
set(VAR "from_child" PARENT_SCOPE) # 修改父目录的message(STATUS "子目录:结束时 VAR='${VAR}'")
我们可以通过下面这个命令来一键搭建出这个目录结构
mkdir -p demo/sub && cat > demo/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.15)
project(DirScopeDemo)set(VAR "outer") # 定义父目录变量message(STATUS "顶层:初始 VAR='${VAR}'")add_subdirectory(sub) # 进入子目录message(STATUS "顶层:子目录结束后 VAR='${VAR}'")
EOFcat > demo/sub/CMakeLists.txt <<'EOF'
message(STATUS "子目录:进入时 VAR='${VAR}'")set(VAR "inner") # 修改本地副本
set(VAR "from_child" PARENT_SCOPE) # 修改父目录的message(STATUS "子目录:结束时 VAR='${VAR}'")
EOF
我们现在就来构建项目
rm -rf build && mkdir build && cd build && cmake ..
输出大概是:
大家看看:其实这3个红箭头打印的是同一个变量,黄色的打印的是另外一个变量。
怎么样?是不是有点好奇了啊!
我们来分点详细解析这个 CMake 例子中蕴含的核心机制:
1. 顶层作用域的初始化
-
当 CMake 开始处理顶层的
CMakeLists.txt
文件时,会创建一个初始的作用域(称为父作用域)。 -
set(VAR "outer")
命令在这个父作用域中定义了一个变量VAR
并为其赋值"outer"
。 -
第一条
message
命令打印出父作用域中VAR
的值,确认其初始状态,输出为顶层:初始 VAR='outer'
。
2. 进入子目录与作用域创建
-
add_subdirectory(sub)
命令指示 CMake 暂停当前执行,转而去处理指定子目录中的CMakeLists.txt
。 -
为了执行子目录中的内容,CMake 会创建一个全新的、独立的子作用域。
-
在创建子作用域时,会发生变量继承:父作用域中的所有变量都会被复制到子作用域中,作为其初始状态。因此,子作用域中的
VAR
初始值也是"outer"
。
3. 在子作用域内的操作与关键区别
-
在子作用域中,第一条
message
命令打印的是其本地继承来的VAR
值,输出为子目录:进入时 VAR='outer'
。 -
随后执行的两个
set
命令展现了核心机制:-
set(VAR "inner")
:此命令仅修改子作用域自身的变量副本。它将子作用域本地的VAR
值从"outer"
改为"inner"
。这个操作完全被隔离在子作用域内,父作用域中的VAR
变量不受影响,其值仍为"outer"
。 -
set(VAR "from_child" PARENT_SCOPE)
:此命令由于使用了PARENT_SCOPE
关键字,其行为完全不同。它不操作当前子作用域的变量,而是向上修改其父作用域中的VAR
变量,将其值设置为"from_child"
。执行后,父作用域的VAR
值变为"from_child"
,而子作用域本地的VAR
值保持不变,仍然是"inner"
。
-
-
因此,子作用域中的第二条
message
命令打印其本地变量,输出为子目录:结束时 VAR='inner'
。
4. 返回顶层与作用域销毁
-
当子目录的
CMakeLists.txt
执行完毕后,CMake 退出子作用域,返回到父作用域继续执行。 -
子作用域及其内部的所有变量(包括
VAR = "inner"
) 被完全销毁,就像这个临时的“房间”被拆除了一样。 -
此时,只有父作用域及其变量继续存在。
5. 最终结果验证
-
CMake 在父作用域中执行最后一条
message
命令。 -
它查询的是父作用域自身的
VAR
变量。这个变量的值已在之前被PARENT_SCOPE
机制修改为"from_child"
。 -
因此,最终输出为
顶层:子目录结束后 VAR='from_child'
,清晰地证明了子目录成功地将新值传递回了父级作用域。
核心机制总结:
-
默认隔离性:
add_subdirectory()
创建的新作用域是隔离的沙箱,其内部的普通变量操作无法直接影响父作用域。 -
单向继承:子作用域通过复制获取父作用域变量的初始值,但之后的修改相互独立。
-
向上通信机制:
PARENT_SCOPE
关键字是子作用域向父作用域传递信息的专属通道,它允许子作用域精确修改父级变量,而不影响其自身的本地副本。
1.2.示例2
📂 目录结构
demo/
├── CMakeLists.txt
└── sub/├── CMakeLists.txt└── subsub/└── CMakeLists.txt
🔹 顶层 demo/CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(DirScopeDemoMulti)set(VAR "outer") # 定义顶层变量message("顶层:初始 VAR='${VAR}'")add_subdirectory(sub) # 进入子目录message("顶层:sub 结束后 VAR='${VAR}'")
🔹 子目录 demo/sub/CMakeLists.txt
message("子目录:进入时 VAR='${VAR}'")set(VAR "inner_sub") # 修改子目录本地副本# 引入孙子目录
add_subdirectory(subsub)message("子目录:孙子目录结束后 VAR='${VAR}'")set(VAR "from_sub" PARENT_SCOPE) # 修改顶层的变量message("子目录:结束时 VAR='${VAR}'")
🔹 孙子目录 demo/sub/subsub/CMakeLists.txt
message("孙子目录:进入时 VAR='${VAR}'")set(VAR "inner_subsub") # 修改孙子目录本地副本
set(VAR "from_subsub" PARENT_SCOPE) # 修改父级(子目录)的变量message("孙子目录:结束时 VAR='${VAR}'")
其实我们可以一键复制下面的代码来一键搭建这个目录结构和文件
mkdir -p demo/sub/subsub && cat > demo/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.15)
project(DirScopeDemoMulti)set(VAR "outer") # 定义顶层变量message("顶层:初始 VAR='${VAR}'")add_subdirectory(sub) # 进入子目录message("顶层:sub 结束后 VAR='${VAR}'")
EOFcat > demo/sub/CMakeLists.txt <<'EOF'
message("子目录:进入时 VAR='${VAR}'")set(VAR "inner_sub") # 修改子目录本地副本# 引入孙子目录
add_subdirectory(subsub)message("子目录:孙子目录结束后 VAR='${VAR}'")set(VAR "from_sub" PARENT_SCOPE) # 修改顶层的变量message("子目录:结束时 VAR='${VAR}'")
EOFcat > demo/sub/subsub/CMakeLists.txt <<'EOF'
message("孙子目录:进入时 VAR='${VAR}'")set(VAR "inner_subsub") # 修改孙子目录本地副本
set(VAR "from_subsub" PARENT_SCOPE) # 修改父级(子目录)的变量message("孙子目录:结束时 VAR='${VAR}'")
EOF
接下来我们就来构建一下项目
rm -rf build && mkdir build && cd build && cmake ..
1. 顶层作用域(demo/
)初始化
- CMake 开始处理顶层
CMakeLists.txt
,创建顶层作用域。 set(VAR "outer")
在顶层作用域中创建变量VAR
。- 输出:
顶层:初始 VAR='outer'
(✅ 打印顶层作用域的VAR
)
2. 进入子目录作用域(demo/sub/
)
add_subdirectory(sub)
触发创建子目录作用域。- 变量继承:子目录作用域从它的父作用域(顶层)复制变量。此时子目录作用域内的
VAR = "outer"
。 - 输出:
子目录:进入时 VAR='outer'
(✅ 打印刚继承来的值)
3. 在子目录作用域内修改本地副本
set(VAR "inner_sub")
修改了子目录作用域自己的VAR
副本。- 此时,顶层作用域的
VAR
仍然是"outer"
,未被影响。 - 子目录作用域的
VAR
变为"inner_sub"
。
4. 进入孙子目录作用域(demo/sub/subsub/
)
add_subdirectory(subsub)
触发创建孙子目录作用域。- 变量继承:孙子目录作用域从它的直接父作用域(子目录) 复制变量。注意,此时子目录的
VAR
已经是"inner_sub"
,所以孙子目录继承到的初始值也是"inner_sub"
。它不会跨越层级直接看到顶层作用域的"outer"
。 - 输出:
孙子目录:进入时 VAR='inner_sub'
(✅ 打印从直接父作用域继承来的值)
5. 在孙子目录作用域内操作
set(VAR "inner_subsub")
修改了孙子目录作用域自己的VAR
副本。- 此时,孙子目录的
VAR
变为"inner_subsub"
。其父作用域(子目录)的VAR
仍然是"inner_sub"
,未被影响。 set(VAR "from_subsub" PARENT_SCOPE)
使用了PARENT_SCOPE
。这条命令的目标是修改孙子作用域的直接父作用域,也就是子目录作用域的VAR
变量。执行后,子目录作用域的VAR
被修改为"from_subsub"
。- 关键点:这条命令不会修改孙子目录自身的
VAR
,也不会修改顶层作用域的VAR
。 - 输出:
孙子目录:结束时 VAR='inner_subsub'
(✅ 打印孙子目录自己的、未被PARENT_SCOPE
改变的本地变量)
6. 返回子目录作用域
- 孙子目录
CMakeLists.txt
执行完毕,孙子作用域被销毁。其中的VAR = "inner_subsub"
也随之消失。 - CMake 回到子目录作用域继续执行。
- 下一条命令是
message(...)
,它打印当前子目录作用域的VAR
。这个变量刚刚被孙子目录通过PARENT_SCOPE
修改为了"from_subsub"
。 - 输出:
子目录:孙子目录结束后 VAR='from_subsub'
(✅ 证明孙子目录成功修改了父级变量)
7. 在子目录作用域内向上通信
set(VAR "from_sub" PARENT_SCOPE)
使用了PARENT_SCOPE
。这条命令的目标是修改子作用域的直接父作用域,也就是顶层作用域的VAR
变量。执行后,顶层作用域的VAR
被修改为"from_sub"
。- 关键点:这条命令不会修改子目录自身的
VAR
。 - 输出:
子目录:结束时 VAR='from_subsub'
(✅ 打印子目录自己的、未被最后一个set
改变的变量。它仍然是"from_subsub"
)
8. 返回顶层作用域
- 子目录
CMakeLists.txt
执行完毕,子作用域被销毁。其中的VAR = "from_subsub"
也随之消失。 - CMake 回到顶层作用域继续执行。
- 最后一条
message(...)
命令打印当前顶层作用域的VAR
。这个变量刚刚被子目录通过PARENT_SCOPE
修改为了"from_sub"
。 - 输出:
顶层:sub 结束后 VAR='from_sub'
(✅ 证明子目录成功修改了其父级(顶层)的变量)
怎么样?是不是很有意思!!
1.3.示例3——孙子作用域会继承父作用域的变量吗?
当您使用 add_subdirectory
引入一个子目录时,CMake 会为这个子目录创建一个新的作用域。在这个新作用域被创建的那一刻,它会自动获得一份父作用域所有变量的拷贝。
-
父作用域:调用
add_subdirectory
的那个 CMakeLists.txt 文件所在的作用域。 -
子作用域:被引入的、子目录中的 CMakeLists.txt 文件将运行在其中的新作用域。
这意味着,在子目录的 CMakeLists.txt 中,你一开头就可以直接读取父作用域中定义的所有变量。
那么如果引入一个孙子作用域呢?在孙子目录的 CMakeLists.txt 中,你一开头就可以直接读取父作用域中定义的所有变量吗?
我们看看
目标:在 顶层、子目录、孙子目录 各自定义一个变量,看看最底层(孙子目录)的 CMakeLists.txt
能不能访问它们。
📂 目录结构
demo/
├── CMakeLists.txt
└── sub/├── CMakeLists.txt└── subsub/└── CMakeLists.txt
🔹 顶层 demo/CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(MultiVarScopeDemo)set(VAR_TOP "from_top") # 顶层变量message("顶层:VAR_TOP='${VAR_TOP}'")add_subdirectory(sub)
🔹 子目录 demo/sub/CMakeLists.txt
set(VAR_SUB "from_sub") # 子目录变量message("子目录:VAR_TOP='${VAR_TOP}', VAR_SUB='${VAR_SUB}'")add_subdirectory(subsub)
🔹 孙子目录 demo/sub/subsub/CMakeLists.txt
set(VAR_SUBSUB "from_subsub") # 孙子目录变量message("孙子目录:VAR_TOP='${VAR_TOP}', VAR_SUB='${VAR_SUB}', VAR_SUBSUB='${VAR_SUBSUB}'")
其实我们可以通过一个连着的bash语句来搭建这个目录结构和文件
mkdir -p demo/sub/subsub && \
cat > demo/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.15)
project(MultiVarScopeDemo)set(VAR_TOP "from_top") # 顶层变量message("顶层:VAR_TOP='${VAR_TOP}'")add_subdirectory(sub)
EOF
cat > demo/sub/CMakeLists.txt <<'EOF'
set(VAR_SUB "from_sub") # 子目录变量message("子目录:VAR_TOP='${VAR_TOP}', VAR_SUB='${VAR_SUB}'")add_subdirectory(subsub)
EOF
cat > demo/sub/subsub/CMakeLists.txt <<'EOF'
set(VAR_SUBSUB "from_subsub") # 孙子目录变量message("孙子目录:VAR_TOP='${VAR_TOP}', VAR_SUB='${VAR_SUB}', VAR_SUBSUB='${VAR_SUBSUB}'")
EOF
接下来我们就来构建我们的项目
rm -rf build && mkdir build && cd build && cmake ..
我们仔细看就会发现
-
每一层都会继承上一层的变量,所以 孙子目录能看到顶层和子目录里定义的变量。
-
在孙子目录里再定义新变量
VAR_SUBSUB
,它只属于孙子目录,父目录看不到。 -
继承方向是自上而下,修改不会自动往回传递。
1.4.示例4—— PARENT_SCOPE
只作用到直接父级
有时候我们确实需要在子目录中计算一个值,然后让父作用域知道。由于默认的隔离行为,直接 set
是行不通的。CMake 提供了 PARENT_SCOPE
选项来实现这个功能。
-
set(MY_VAR “Value” PARENT_SCOPE)
-
这条命令不会改变当前子作用域的
MY_VAR
。 -
它会去修改父作用域中的
MY_VAR
。
-
我们就在刚才那个例子的基础上,加一个 反向修改顶层变量 的实验,用 PARENT_SCOPE
。
📂 目录结构
demo/
├── CMakeLists.txt
└── sub/├── CMakeLists.txt└── subsub/└── CMakeLists.txt
🔹 顶层 demo/CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(MultiVarScopeDemo)set(VAR_TOP "from_top") # 顶层变量message("顶层:初始 VAR_TOP='${VAR_TOP}'")add_subdirectory(sub)message("顶层:sub 执行结束后 VAR_TOP='${VAR_TOP}'")
🔹 子目录 demo/sub/CMakeLists.txt
set(VAR_SUB "from_sub") # 子目录变量message("子目录:VAR_TOP='${VAR_TOP}', VAR_SUB='${VAR_SUB}'")add_subdirectory(subsub)message("子目录:subsub 执行结束后 VAR_TOP='${VAR_TOP}', VAR_SUB='${VAR_SUB}'")
🔹 孙子目录 demo/sub/subsub/CMakeLists.txt
set(VAR_SUBSUB "from_subsub") # 孙子目录变量message("孙子目录:进入时 VAR_TOP='${VAR_TOP}', VAR_SUB='${VAR_SUB}', VAR_SUBSUB='${VAR_SUBSUB}'")# 尝试修改父级作用域(子目录)的变量
set(VAR_SUB "changed_by_subsub" PARENT_SCOPE)# 尝试修改父级作用域(子目录)里的 VAR_TOP
# 注意:这只会改“子目录”的副本,不会直接作用到顶层
set(VAR_TOP "changed_by_subsub" PARENT_SCOPE)message("孙子目录:结束时 VAR_TOP='${VAR_TOP}', VAR_SUB='${VAR_SUB}', VAR_SUBSUB='${VAR_SUBSUB}'")
其实我们可以通过下面这个命令来一键搭建出这个目录结构
mkdir -p demo/sub/subsub && \
cat > demo/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.15)
project(MultiVarScopeDemo)set(VAR_TOP "from_top") # 顶层变量message("顶层:初始 VAR_TOP='${VAR_TOP}'")add_subdirectory(sub)message("顶层:sub 执行结束后 VAR_TOP='${VAR_TOP}'")
EOF
cat > demo/sub/CMakeLists.txt <<'EOF'
set(VAR_SUB "from_sub") # 子目录变量message("子目录:VAR_TOP='${VAR_TOP}', VAR_SUB='${VAR_SUB}'")add_subdirectory(subsub)message("子目录:subsub 执行结束后 VAR_TOP='${VAR_TOP}', VAR_SUB='${VAR_SUB}'")
EOF
cat > demo/sub/subsub/CMakeLists.txt <<'EOF'
set(VAR_SUBSUB "from_subsub") # 孙子目录变量message("孙子目录:进入时 VAR_TOP='${VAR_TOP}', VAR_SUB='${VAR_SUB}', VAR_SUBSUB='${VAR_SUBSUB}'")# 尝试修改父级作用域(子目录)的变量
set(VAR_SUB "changed_by_subsub" PARENT_SCOPE)# 尝试修改父级作用域(子目录)里的 VAR_TOP
# 注意:这只会改“子目录”的副本,不会直接作用到顶层
set(VAR_TOP "changed_by_subsub" PARENT_SCOPE)message("孙子目录:结束时 VAR_TOP='${VAR_TOP}', VAR_SUB='${VAR_SUB}', VAR_SUBSUB='${VAR_SUBSUB}'")
EOF
接下来我们就来构建这个项目
rm -rf build && mkdir build && cd build && cmake ..
大家仔细观察上面这个,是不是很有意思
其实啊!!
-
孙子目录可以用
PARENT_SCOPE
修改直接父目录(sub)的变量。-
VAR_SUB
被改成"changed_by_subsub"
。 -
VAR_TOP
在子目录副本里变成"changed_by_subsub"
。
-
-
顶层的
VAR_TOP
没变,仍然是"from_top"
。-
因为
PARENT_SCOPE
只作用到直接父级(这里是子目录),不会层层冒泡到顶层。
-
1.5. 特殊的“缓存变量”(Cache Variables)
我们这里说的目录作用域是针对普通变量的。
目录作用域的规则有一个重要的例外:缓存变量(通过 set(... CACHE ...)
设置的变量)。
-
缓存变量是全局的,存在于所有目录作用域之上。
-
任何作用域对缓存变量的读写操作都是针对同一个全局实体。
-
因此,缓存变量的行为不遵循目录作用域的隔离规则。
我们会在接下来的文章中讲述缓存变量。我们到那里再讲讲缓存变量在目录作用域的表现。