【CMake】缓存变量

发布于:2025-09-08 ⋅ 阅读:(23) ⋅ 点赞:(0)

目录

一. 缓存变量

二.创建缓存变量

2.1.使用set()来创建缓存变量

2.2.使用FORCE参数来覆盖缓存变量

2.2.1.示例1——不带force的set是不能覆盖已经存在的缓存变量的

2.2.2.示例2——带force的set才能覆盖已经存在的缓存变量

2.2.3.对比示例

2.3.命令行 -D 创建/覆盖缓存变量

2.3.1.直接使用-D来创建/覆盖缓存变量

2.3.2.使用-d来替换掉CMakeLists.txt里面指定的缓存变量

三.缓存变量的作用域

3.1.示例1——全局可见行和全局唯一性

3.2.示例2——全局可见性

四. 缓存变量与普通变量的交互:优先级规则

4.1.示例1——普通变量的设置会“遮盖”缓存变量

4.2.示例2——普通变量的遮盖效应会传递到子作用域


一. 缓存变量

我们去官网看看是怎么说的:set — CMake 4.1.1 Documentation

翻译下来也大概就是下面这样子。

设置 CMake 缓存条目 (Set Cache Entry)

set(<变量名> <值>... CACHE <类型> <说明文字> [FORCE])

这条命令用于在 CMake 中创建或修改一个缓存变量。您可以把缓存变量想象成项目的配置设置,这些设置会被保存下来(在 CMakeCache.txt 文件中),以便下次运行 CMake 时记住用户的选择。正因为它们旨在让用户自定义,所以默认情况下,如果该缓存条目已经存在,set 命令不会覆盖它。如果您希望强制覆盖现有的值,请使用 FORCE 选项。

参数详解:

  • <类型> (必须指定): 定义了变量的类型,它决定了在 cmake-gui 等图形化工具中如何与用户交互。必须是以下类型之一:

    • BOOL: 布尔值,只能是 ON 或 OFF。例如,用来控制是否编译某个功能模块。在 cmake-gui 中会显示为一个复选框,非常直观。

    • FILEPATH: 指向磁盘上某个文件的路径。例如,指定一个外部工具的路径。在 cmake-gui 中会提供一个文件选择对话框,让用户方便地浏览和选择。

    • PATH: 指向磁盘上某个目录的路径。例如,指定第三方库的安装目录。在 cmake-gui 中同样会提供一个目录选择对话框

    • STRING: 一行普通的文本。在 cmake-gui 中显示为一个文本框。如果您还通过 set_property(CACHE <变量名> PROPERTY STRINGS ...) 设置了可选值列表,它则会变成一个下拉选择框,让用户从预定义的选项中选择。

    • INTERNAL: 也是一行文本,但主要用于 CMake 内部使用。这种类型的变量不会显示在 cmake-gui 中,用户无法看到或修改。它通常用于在多次运行 CMake 时在内部持久化存储一些状态或信息。注意:使用此类型隐含了 FORCE 选项,即会自动覆盖旧值。

  • <说明文字> (必须指定): 这是一段简单的描述文字,用于解释这个缓存变量的作用和目的。当用户在 cmake-gui 中将鼠标悬停在变量上时,这段文字就会显示出来,帮助用户理解该如何配置。请务必写得清晰明了。

  • [FORCE] (可选): 就像上面提到的,加上这个选项会强制用新值覆盖已经存在的缓存条目。如果你确信无论之前用户设置成什么,都需要被当前脚本中的值重置,那就使用它。

重要注意事项 (非常重要!):

  1. 变量覆盖规则 (优先级): CMake 中变量的查找规则是:普通变量会覆盖未使用的缓存变量。这意味着,如果你之前用 set(MY_VAR "value")(没有 CACHE)设置了一个普通变量,那么直接读取 MY_VAR 得到的是普通变量的值,而不是缓存变量的值。要访问缓存变量,需要使用 $CACHE{MY_VAR} 语法(CMake 3.13及以上版本)。这是一个非常常见的困惑点!

  2. 处理命令行创建的变量: 用户可能在运行 CMake 时通过 -D<变量>=<值> 的命令行选项创建了一个缓存变量,但没有指定类型。此时,set(... CACHE ...) 命令会为其补充上类型

  3. 路径转换: 如果一个通过 -D 创建的、类型为 PATH 或 FILEPATH 的缓存变量,其值是相对路径(如 ../mylib),那么当 set 命令为其显式设置类型时,CMake 会自动将这个相对路径转换为基于当前工作目录的绝对路径,从而保证路径的准确性。

二.创建缓存变量

2.1.使用set()来创建缓存变量

📂 目录结构

demo/
└── CMakeLists.txt

🔹 demo/CMakeLists.txt

cmake_minimum_required(VERSION 3.15)
project(CacheVarDemo)

# 1. 创建一些缓存变量(会写入 build/CMakeCache.txt)
set(MY_BOOL  ON  CACHE BOOL     "是否启用某个功能模块")
set(MY_PATH  "../mylib" CACHE PATH     "第三方库路径")
set(MY_FILE  "../README.md" CACHE FILEPATH "某个文件路径")
set(MY_STR   "hello" CACHE STRING  "一段字符串")

# 2. 显示结果
message("MY_BOOL='${MY_BOOL}'")
message("MY_PATH='${MY_PATH}'")
message("MY_FILE='${MY_FILE}'")
message("MY_STR='${MY_STR}'")

其实我们可以一键运行下面这个命令来进行搭建这个目录结构

mkdir -p demo && cat > demo/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.15)
project(CacheVarDemo)

# 1. 创建一些缓存变量(会写入 build/CMakeCache.txt)
set(MY_BOOL  ON  CACHE BOOL     "是否启用某个功能模块")
set(MY_PATH  "../mylib" CACHE PATH     "第三方库路径")
set(MY_FILE  "../README.md" CACHE FILEPATH "某个文件路径")
set(MY_STR   "hello" CACHE STRING  "一段字符串")

# 2. 显示结果
message("MY_BOOL='${MY_BOOL}'")
message("MY_PATH='${MY_PATH}'")
message("MY_FILE='${MY_FILE}'")
message("MY_STR='${MY_STR}'")
EOF

demo 目录下执行:

rm -rf build && mkdir build && cd build && cmake ..

大家仔细看看

几乎所有的缓存变量都会被记录到 CMakeCache.txt 文件中。 这个文件本质上就是一个持久化的键值对存储,CMake 的主要目的就是通过这个文件来记住用户的配置选择,从而实现“一次配置,多次使用”,避免每次运行时都重新询问所有配置。

我们现在就去查看CMakeCache.txt中的内容

嗯?怎么这么多别的变量啊?

CMakeCache.txt 文件不仅仅存储了通过 set(... CACHE ...) 或 -D 选项显式设置的变量,它更是一个庞大的数据库,存储了CMake在配置和生成过程中产生的、为了确保下次运行一致所必需的大量内部缓存变量。

那么我们怎么查询我们设置的缓存变量呢?其实我们可以借助grep

grep MY_ CMakeCache.txt

意思是:

  • grep:Linux/Unix 下的文本搜索工具。

  • MY_:要搜索的关键字(这里是匹配所有以 MY_ 开头的变量名)。

  • CMakeCache.txt:要搜索的文件,就是 CMake 生成的缓存文件。

怎么样?还是很容易理解的吧。

2.2.使用FORCE参数来覆盖缓存变量

我们来好好了解一下

set(<variable> <value> CACHE <type> <docstring> [FORCE]) 命令的执行遵循一套严格的规则:

场景一:缓存变量不存在(初次运行)

  • 触发条件:当你第一次在一个空的构建目录中运行 CMake 时,CMakeCache.txt 文件不存在或其中没有名为 <variable> 的条目。

  • CMake 的决策逻辑

    1. 检查:在缓存中查找 <variable>,结果是没找到。

    2. 行动:“用户显然还没有机会设置这个变量。我将把我(开发者)提供的 <value> 作为初始值,并将其持久化到缓存中。”

  • 最终结果:缓存变量 被创建,其值被设置为命令中提供的 <value>。这个值会被写入 CMakeCache.txt 文件,成为一个正式的、可供用户修改的配置选项。

  • 比喻:你提供了一份合同的初稿,因为还没有正式版本,所以初稿直接被采纳为正式合同。

场景二:缓存变量已存在(后续运行)

  • 触发条件:在后续的配置运行中,CMakeCache.txt 文件已经存在,并且包含了名为 <variable> 的条目。它的值可能是:

    • 之前设置的默认值。

    • 用户通过 cmake-gui / ccmake 修改后的值。

    • 用户通过命令行 -D<variable>=<new_value> 设置的值。

  • CMake 的决策逻辑(无 FORCE 关键字)

    1. 检查:在缓存中查找 <variable>,结果“命中”!其当前值为 [cached_value]

    2. 决策:“这个变量已经存在了。这意味着用户可能已经看到了它,并且有机会做出自己的选择。我的职责是提供一个默认值,而不是一个强制值。 既然用户没有表达修改的意图(这次运行没有用新的 -D 重新指定),那么我应该继续信任并保留缓存中现有的值。”

    3. 行动忽略本次 set 命令中提供的 <value>

  • 最终结果:缓存变量的值 保持不变。这次 set 命令实际上成了一个“空操作”。

  • 比喻:用户已经在你给的合同初稿上做了修改并签了字。你不会拿一份新的空白的初稿覆盖掉已经签署的合同。你尊重用户的最终决定。

场景三:强制覆盖(使用 FORCE 关键字)

  • 触发条件:在命令中使用了 FORCE 选项:set(... CACHE ... FORCE)

  • CMake 的决策逻辑

    1. 检查:在缓存中查找 <variable>,结果“命中”!其当前值为 [old_value]

    2. 决策:“虽然这个变量已经存在,但命令中包含了 FORCE 关键字!这说明开发者明确意图要覆盖当前的值,无论用户之前设置过什么。”

    3. 行动:使用本次 set 命令中提供的 <value> 覆盖缓存中现有的值。

  • 最终结果:缓存变量的值 被强制更新 为命令中提供的新 <value>

  • 比喻:这是一个“单方面修订条款”的行为。无论合同之前是什么状态,现在都用这份新的版本强制替换它。

2.2.1.示例1——不带force的set是不能覆盖已经存在的缓存变量的

话不多说,我们先看例子


📂 目录结构

demo/
└── CMakeLists.txt

🔹 demo/CMakeLists.txt

cmake_minimum_required(VERSION 3.15)
project(CacheNoForceDemo)

# 第一次设置缓存变量
set(MY_VAR "first" CACHE STRING "测试变量")
message("第一次设置后 MY_VAR='${MY_VAR}'")

# 第二次尝试覆盖(不带 FORCE)
set(MY_VAR "second" CACHE STRING "测试变量")
message("第二次尝试覆盖后 MY_VAR='${MY_VAR}'")

我们可以通过下面这一条连着的bash语句来搭建这个目录结构和文件

mkdir -p demo && \
cat > demo/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.15)
project(CacheNoForceDemo)

# 第一次设置缓存变量
set(MY_VAR "first" CACHE STRING "测试变量")
message("第一次设置后 MY_VAR='${MY_VAR}'")

# 第二次尝试覆盖(不带 FORCE)
set(MY_VAR "second" CACHE STRING "测试变量")
message("第二次尝试覆盖后 MY_VAR='${MY_VAR}'")
EOF

接下来我们就来构建这个项目

mkdir build && cd build && cmake ..

我们发现覆盖前后都是一样的。也就是说,我们的覆盖是失败的。

2.2.2.示例2——带force的set才能覆盖已经存在的缓存变量

好的 👍,我给你一个「对照实验」:同一个项目里,先演示 不带 FORCE 时无法覆盖,再演示 加 FORCE 成功覆盖


📂 目录结构

demo/
└── CMakeLists.txt

🔹 demo/CMakeLists.txt

cmake_minimum_required(VERSION 3.15)
project(CacheForceCompareDemo)

# 第一次设置缓存变量
set(MY_VAR "first" CACHE STRING "测试变量")
message("第一次设置后 MY_VAR='${MY_VAR}'")

# 第二次尝试覆盖(不带 FORCE)
set(MY_VAR "second" CACHE STRING "测试变量")
message("第二次尝试覆盖后 MY_VAR='${MY_VAR}'")

# 第三次尝试覆盖(加 FORCE)
set(MY_VAR "third" CACHE STRING "测试变量" FORCE)
message("第三次使用 FORCE 覆盖后 MY_VAR='${MY_VAR}'")

我们可以通过下面这个命令来一键搭建出这个目录结构和文件

mkdir -p demo && \
cat > demo/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.15)
project(CacheForceCompareDemo)

# 第一次设置缓存变量
set(MY_VAR "first" CACHE STRING "测试变量")
message("第一次设置后 MY_VAR='${MY_VAR}'")

# 第二次尝试覆盖(不带 FORCE)
set(MY_VAR "second" CACHE STRING "测试变量")
message("第二次尝试覆盖后 MY_VAR='${MY_VAR}'")

# 第三次尝试覆盖(加 FORCE)
set(MY_VAR "third" CACHE STRING "测试变量" FORCE)
message("第三次使用 FORCE 覆盖后 MY_VAR='${MY_VAR}'")
EOF

接下来我们就来构建我们的项目

mkdir build && cd build && cmake ..

我们发现,每一次打印的结果都是不一样的,这就更加说明了我们的猜想


  • 不带 FORCE → 已存在的缓存值不会被覆盖。
  • 带 FORCE → 立刻覆盖缓存里的旧值。

2.2.3.对比示例

我们可以写一个例子,用 两个缓存变量,分别演示 不使用 FORCE使用 FORCE 的效果。


📂 目录结构

demo/
└── CMakeLists.txt

🔹 demo/CMakeLists.txt

cmake_minimum_required(VERSION 3.15)
project(CacheTwoVarsDemo)

# 第一次创建两个缓存变量
set(VAR1 "first1" CACHE STRING "第一个缓存变量")
set(VAR2 "first2" CACHE STRING "第二个缓存变量")

# 尝试修改,不加 FORCE
set(VAR1 "second1" CACHE STRING "第一个缓存变量")
set(VAR2 "second2" CACHE STRING "第二个缓存变量" FORCE)

# 打印结果
message("VAR1='${VAR1}' (不使用 FORCE)")
message("VAR2='${VAR2}' (使用 FORCE)")


其实我们可以通过下面这个目录来一键构建出这个目录结构和文件

mkdir -p demo && cat > demo/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.15)
project(CacheTwoVarsDemo)

# 第一次创建两个缓存变量
set(VAR1 "first1" CACHE STRING "第一个缓存变量")
set(VAR2 "first2" CACHE STRING "第二个缓存变量")

# 尝试修改,不加 FORCE
set(VAR1 "second1" CACHE STRING "第一个缓存变量")
set(VAR2 "second2" CACHE STRING "第二个缓存变量" FORCE)

# 打印结果
message("VAR1='${VAR1}' (不使用 FORCE)")
message("VAR2='${VAR2}' (使用 FORCE)")
EOF

接下来我们就来构建项目

rm -rf build && mkdir build && cd build && cmake ..

 🔹 解释

  • VAR1第一次创建是 "first1",之后修改 "second1" 没有 FORCE,所以缓存保持 "first1"
  • VAR2第一次创建是 "first2",修改 "second2" 使用 FORCE,缓存被覆盖成 "second2"。  

我们可以去CMakeChace.txt里面看看

  

和我们的运行结果可是一模一样的。

2.3.命令行 -D 创建/覆盖缓存变量

2.3.1.直接使用-D来创建/覆盖缓存变量

📂 目录结构

demo/
└── CMakeLists.txt

这里的 CMakeLists.txt 可以几乎空,但保留一个最小声明:

cmake_minimum_required(VERSION 3.15)
project(DCacheDemo)

接下来我们就来看看

rm -rf build && mkdir build && cd build

  

接下来我们就使用 -D 创建新缓存变量

cmake .. -DMY_NEW_VAR="hello"

  

我们现在就可以去CMakeCache.txt里面查看这个缓存变量

grep MY_NEW_VAR CMakeCache.txt

  

跟我们设置的一模一样的。


接下来我们将使用 -D 覆盖已有缓存变量

然后运行:

cmake .. -DMY_NEW_VAR="hello world"

  

我们现在就可以去CMakeCache.txt里面查看这个缓存变量

grep MY_NEW_VAR CMakeCache.txt

  

怎么样?

2.3.2.使用-d来替换掉CMakeLists.txt里面指定的缓存变量

📂 目录结构

demo/
└── CMakeLists.txt

🔹 demo/CMakeLists.txt

cmake_minimum_required(VERSION 3.15)
project(DOverrideDemo)

# 定义一个缓存变量
set(MY_OPTION "from_cmakelists" CACHE STRING "演示缓存变量")

# 打印最终结果
message("MY_OPTION='${MY_OPTION}'")

我们可以一键搭建这个项目的目录结构和文件

mkdir -p demo && cat > demo/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.15)
project(DOverrideDemo)

# 定义一个缓存变量
set(MY_OPTION "from_cmakelists" CACHE STRING "演示缓存变量")

# 打印结果
message("MY_OPTION='${MY_OPTION}'")
EOF

接下来我们来构建一下这个项目

rm -rf build && mkdir build && cd build && cmake ..

现在我们可以去CMakeCache.txt 里看看对应条目:

接下来我们使用 -D 覆盖 CMakeLists.txt 中的缓存变量

cmake .. -DMY_OPTION="from_commandline"

这个时候我们回去那个CMakeCache.txt 看看这个条目被更新为:

三.缓存变量的作用域

在 CMake 的变量体系中,缓存变量是一个特殊的存在,它完全超越了普通变量所遵循的“目录作用域”规则。

您可以将其理解为项目配置中的全局变量持久化设置

它的核心特征在于其全局可见性持久化存储,这与普通变量的局部性和临时性形成了鲜明对比。

核心特性:全局唯一与持久化

  1. 全局唯一性(单一事实来源)
    整个 CMake 项目中,任何一个特定的缓存变量有且只有一个。它被存储在一个独立的、全局的存储区中,通常被视为所有目录作用域之上的一个共享层。无论您在当前目录、子目录,还是父目录中读取一个名为 MY_CACHE_VAR 的缓存变量,您访问的都是同一个全局实体。它的值在任何地方、任何时候(在一次配置过程中)都是一致的。

  2. 无视目录作用域隔离
    这是缓存变量与普通变量最根本的区别。普通变量严格遵守目录作用域的“向下继承,向上隔离”规则。而缓存变量则完全无视这堵“墙”。

    • 读操作:在任何目录作用域中读取缓存变量,得到的都是其全局唯一的值。

    • 写操作:在任何目录作用域中修改缓存变量的值,都会立即更新这个全局唯一的值,并且这个更改立刻对所有其他目录作用域可见。在一个子目录中修改了缓存变量,父目录或其他兄弟目录在随后读取它时,会立刻得到这个新值。这彻底打破了普通变量那种“修改互不影响”的隔离性。

  3. 持久化存储(跨运行存在)
    缓存变量的值不会被保存在 CMakeLists.txt 文件里,而是会被写入到 CMake 构建目录下的 CMakeCache.txt 文件中。这个文件是 CMake 的“记忆中心”。这意味着:

    • 一旦一个缓存变量被设置,它的值会在您多次运行 cmake 配置命令的过程中持续存在。

    • 这正是图形化配置工具(如 cmake-gui 或 ccmake)能够展示并允许用户修改的变量列表。

    • 要清除一个缓存变量,必须手动删除构建目录、在 GUI 中操作,或使用 unset(... CACHE) 命令。

3.1.示例1——全局可见行和全局唯一性

📂 目录结构

demo/
├── CMakeLists.txt
└── sub/
    ├── CMakeLists.txt
    └── subsub/
        └── CMakeLists.txt

🔹 顶层 demo/CMakeLists.txt

cmake_minimum_required(VERSION 3.15)
project(GlobalCacheVarDemo)

set(MY_CACHE_VAR "from_top" CACHE STRING "演示全局缓存变量" FORCE)
message("顶层看到 MY_CACHE_VAR='${MY_CACHE_VAR}'")

add_subdirectory(sub)

message("顶层再次看到 MY_CACHE_VAR='${MY_CACHE_VAR}'")

🔹 子目录 demo/sub/CMakeLists.txt

message("子目录进入时 MY_CACHE_VAR='${MY_CACHE_VAR}'")

set(MY_CACHE_VAR "from_sub" CACHE STRING "演示全局缓存变量" FORCE)
message("子目录修改后 MY_CACHE_VAR='${MY_CACHE_VAR}'")

add_subdirectory(subsub)

# 在孙子目录修改完之后,再次查看
message("子目录返回后 MY_CACHE_VAR='${MY_CACHE_VAR}'")

🔹 孙子目录 demo/sub/subsub/CMakeLists.txt

message("孙子目录进入时 MY_CACHE_VAR='${MY_CACHE_VAR}'")

set(MY_CACHE_VAR "from_subsub" CACHE STRING "演示全局缓存变量" FORCE)
message("孙子目录修改后 MY_CACHE_VAR='${MY_CACHE_VAR}'")

其实我们可以通过一行 bash(一次性创建文件并运行)来快速搭建这个目录结构和文件。

mkdir -p demo/sub/subsub && cat > demo/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.15)
project(GlobalCacheVarDemo)

set(MY_CACHE_VAR "from_top" CACHE STRING "演示全局缓存变量" FORCE)
message("顶层看到 MY_CACHE_VAR='${MY_CACHE_VAR}'")

add_subdirectory(sub)

message("顶层再次看到 MY_CACHE_VAR='${MY_CACHE_VAR}'")
EOF

cat > demo/sub/CMakeLists.txt <<'EOF'
message("子目录进入时 MY_CACHE_VAR='${MY_CACHE_VAR}'")

set(MY_CACHE_VAR "from_sub" CACHE STRING "演示全局缓存变量" FORCE)
message("子目录修改后 MY_CACHE_VAR='${MY_CACHE_VAR}'")

add_subdirectory(subsub)

message("子目录返回后 MY_CACHE_VAR='${MY_CACHE_VAR}'")
EOF

cat > demo/sub/subsub/CMakeLists.txt <<'EOF'
message("孙子目录进入时 MY_CACHE_VAR='${MY_CACHE_VAR}'")

set(MY_CACHE_VAR "from_subsub" CACHE STRING "演示全局缓存变量" FORCE)
message("孙子目录修改后 MY_CACHE_VAR='${MY_CACHE_VAR}'")
EOF

接下来我们就来构建我们的项目

mkdir build && cd build && cmake ..

大家仔细观察,就会发现这3个CMakeLists.txt里面操作的都是同一个缓存变量!!!!这就验证了缓存变量的全局可见性和全局唯一性。

注意:如果我们不在set里面加force,运行结果就会是下面这样子。

3.2.示例2——全局可见性

接下来我们将

  • 每一层目录都各自 set 一个不同名字的缓存变量(例如 TOP_CACHE_VARSUB_CACHE_VARSUBSUB_CACHE_VAR)。

  • 并且在 每一层打印出 全部 3 个变量,直观演示「缓存变量是全局唯一的」特性。


📂 目录结构

demo/
├── CMakeLists.txt
└── sub/
    ├── CMakeLists.txt
    └── subsub/
        └── CMakeLists.txt

🔹 demo/CMakeLists.txt (顶层)

cmake_minimum_required(VERSION 3.15)
project(CacheThreeVarsDemo)

# 顶层设置一个缓存变量
set(TOP_CACHE_VAR "set_in_top" CACHE STRING "顶层缓存变量")

message("[顶层] 进入时: TOP='${TOP_CACHE_VAR}', SUB='${SUB_CACHE_VAR}', SUBSUB='${SUBSUB_CACHE_VAR}'")

# 进入子目录
add_subdirectory(sub)

message("[顶层] 返回时: TOP='${TOP_CACHE_VAR}', SUB='${SUB_CACHE_VAR}', SUBSUB='${SUBSUB_CACHE_VAR}'")

🔹 demo/sub/CMakeLists.txt (子目录)

message("[子目录] 进入时: TOP='${TOP_CACHE_VAR}', SUB='${SUB_CACHE_VAR}', SUBSUB='${SUBSUB_CACHE_VAR}'")

# 设置子目录自己的缓存变量
set(SUB_CACHE_VAR "set_in_sub" CACHE STRING "子目录缓存变量")

message("[子目录] 设置后: TOP='${TOP_CACHE_VAR}', SUB='${SUB_CACHE_VAR}', SUBSUB='${SUBSUB_CACHE_VAR}'")

# 进入孙子目录
add_subdirectory(subsub)

message("[子目录] 从孙子返回时: TOP='${TOP_CACHE_VAR}', SUB='${SUB_CACHE_VAR}', SUBSUB='${SUBSUB_CACHE_VAR}'")

🔹 demo/sub/subsub/CMakeLists.txt (孙子目录)

message("[孙子目录] 进入时: TOP='${TOP_CACHE_VAR}', SUB='${SUB_CACHE_VAR}', SUBSUB='${SUBSUB_CACHE_VAR}'")

# 设置孙子目录自己的缓存变量
set(SUBSUB_CACHE_VAR "set_in_subsub" CACHE STRING "孙子目录缓存变量")

message("[孙子目录] 设置后: TOP='${TOP_CACHE_VAR}', SUB='${SUB_CACHE_VAR}', SUBSUB='${SUBSUB_CACHE_VAR}'")

我们可以一键复制下面的 Bash 语句来创建我们的目录结构和文件

mkdir -p demo/sub/subsub && cat > demo/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.15)
project(CacheThreeVarsDemo)

set(TOP_CACHE_VAR "set_in_top" CACHE STRING "顶层缓存变量")
message("[顶层] 进入时: TOP='${TOP_CACHE_VAR}', SUB='${SUB_CACHE_VAR}', SUBSUB='${SUBSUB_CACHE_VAR}'")
add_subdirectory(sub)
message("[顶层] 返回时: TOP='${TOP_CACHE_VAR}', SUB='${SUB_CACHE_VAR}', SUBSUB='${SUBSUB_CACHE_VAR}'")
EOF

cat > demo/sub/CMakeLists.txt <<'EOF'
message("[子目录] 进入时: TOP='${TOP_CACHE_VAR}', SUB='${SUB_CACHE_VAR}', SUBSUB='${SUBSUB_CACHE_VAR}'")
set(SUB_CACHE_VAR "set_in_sub" CACHE STRING "子目录缓存变量")
message("[子目录] 设置后: TOP='${TOP_CACHE_VAR}', SUB='${SUB_CACHE_VAR}', SUBSUB='${SUBSUB_CACHE_VAR}'")
add_subdirectory(subsub)
message("[子目录] 从孙子返回时: TOP='${TOP_CACHE_VAR}', SUB='${SUB_CACHE_VAR}', SUBSUB='${SUBSUB_CACHE_VAR}'")
EOF

cat > demo/sub/subsub/CMakeLists.txt <<'EOF'
message("[孙子目录] 进入时: TOP='${TOP_CACHE_VAR}', SUB='${SUB_CACHE_VAR}', SUBSUB='${SUBSUB_CACHE_VAR}'")
set(SUBSUB_CACHE_VAR "set_in_subsub" CACHE STRING "孙子目录缓存变量")
message("[孙子目录] 设置后: TOP='${TOP_CACHE_VAR}', SUB='${SUB_CACHE_VAR}', SUBSUB='${SUBSUB_CACHE_VAR}'")
EOF

接下来我们就来构建我们的项目

mkdir build && cd build && cmake ..


这样你就能看到:

  1. 每层定义的缓存变量 对全局都可见

  2. 即使是后设置的变量(SUB_CACHE_VARSUBSUB_CACHE_VAR),顶层最后也能读到它们的值。

要不要我再帮你加一个 普通变量版本(不带 CACHE),并打印对比差异?

四. 缓存变量与普通变量的交互:优先级规则

当一个变量名既作为普通变量存在,又作为缓存变量存在时,CMake 遵循一条明确的优先级规则

  • 普通变量的设置会“遮盖”缓存变量

  •  一旦通过`set(MY_VAR "value")`这样的语句在当前作用域内定义了一个普通变量该作用域及其所有由此向下延伸的子作用域(通过`add_subdirectory()`或`function()`调用进入的新作用域)中,任何对`${MY_VAR}`的求值操作都会直接返回这个新设置的普通变量的值。缓存中存储的值依然完好无损地存在于全局缓存中,只是在当前的变量解析路径上被暂时地“绕过”了。

  • 但是,这个“遮盖”效应是局部的,仅限于当前目录作用域其子作用域。一旦跳出这个范围,仍然可以访问到底层缓存变量的值。

  • 为了提供一种显式且可靠的访问方式,不受当前作用域内普通变量的干扰,CMake引入了`$CACHE{MY_VAR}`语法。这是一种强制的、指向性的访问。如果您使用 $CACHE{MY_VAR} 语法(CMake 3.13+),无论你在哪个作用域,仍然可以访问到底层缓存变量的值。它明确指示CMake解释器绕过所有当前作用域内的普通变量查找,直接访问全局缓存命名空间并获取其中存储的值

4.1.示例1——普通变量的设置会“遮盖”缓存变量

好 👌 我给你写一个最简单的例子,演示 同名普通变量和缓存变量的优先级关系


📂 目录结构

demo/
├── CMakeLists.txt
└── sub/
    └── CMakeLists.txt

🔹 顶层 demo/CMakeLists.txt

cmake_minimum_required(VERSION 3.15)
project(ShadowCacheDemo)

# 定义一个缓存变量
set(MY_VAR "cache_value" CACHE STRING "缓存变量 MY_VAR")

message("[顶层] 初始: MY_VAR='${MY_VAR}' (缓存)")

# 进入子目录
add_subdirectory(sub)

# 回到顶层后,再次读取
message("[顶层] 返回时: MY_VAR='${MY_VAR}' (缓存)")

🔹 子目录 demo/sub/CMakeLists.txt

# 子目录开始
message("[子目录] 进入时: MY_VAR='${MY_VAR}' (继承自缓存)")

# 定义同名的普通变量(遮盖缓存变量)
set(MY_VAR "normal_value")

message("[子目录] 设置普通变量后: MY_VAR='${MY_VAR}' (普通变量覆盖缓存)")

# 如果想显式访问缓存值(CMake 3.13+ 支持)
message("[子目录] 显式访问缓存: $CACHE{MY_VAR}")

🔹 一行 Bash 运行

mkdir -p demo/sub && cat > demo/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.15)
project(ShadowCacheDemo)
set(MY_VAR "cache_value" CACHE STRING "缓存变量 MY_VAR")
message("[顶层] 初始: MY_VAR='${MY_VAR}' (缓存)")
add_subdirectory(sub)
message("[顶层] 返回时: MY_VAR='${MY_VAR}' (缓存)")
EOF

cat > demo/sub/CMakeLists.txt <<'EOF'
message("[子目录] 进入时: MY_VAR='${MY_VAR}' (继承自缓存)")
set(MY_VAR "normal_value")
message("[子目录] 设置普通变量后: MY_VAR='${MY_VAR}' (普通变量覆盖缓存)")
message("[子目录] 显式访问缓存: $CACHE{MY_VAR}")
EOF

接下来我们就来构建我们的项目

mkdir build && cd build && cmake ..

输出

这也就说明了

当一个普通变量被设置成一个与缓存变量同名的名字时,在其作用域内普通变量的值会“遮盖”(Shadow)掉缓存变量的值。这意味着,直接使用 ${MY_VAR} 将会访问到普通变量的值。

但是,缓存变量本身的值并没有被改变,它依然安全地存储在 CMakeCache.txt 中。一旦离开了那个普通变量的作用域(例如,从子目录返回到父目录),${MY_VAR} 又会重新指向那个未被改变的缓存变量的值。

我们来仔细讲解一下

1.顶层目录,初始阶段

  • set(MY_VAR "cache_value" CACHE STRING "缓存变量 MY_VAR")

    • 这行代码定义了一个名为 MY_VAR 的缓存变量,其值为 "cache_value"

  • message("[顶层] 初始: MY_VAR='${MY_VAR}' (缓存)")

    • 此时,${MY_VAR} 读取到的就是这个缓存变量的值。

    • 输出[顶层] 初始: MY_VAR='cache_value' (缓存)

2.进入子目录 sub/

  • add_subdirectory(sub) 命令执行,CMake 开始处理 sub/CMakeLists.txt

  • message("[子目录] 进入时: MY_VAR='${MY_VAR}' (继承自缓存)")

    • 子目录继承了父目录的作用域。此时还没有同名的普通变量,所以 ${MY_VAR} 仍然解析为缓存变量的值。

    • 输出[子目录] 进入时: MY_VAR='cache_value' (继承自缓存)

3.在子目录中设置普通变量

  • set(MY_VAR "normal_value") (注意:没有 CACHE 关键字)

    • 这行代码定义了一个同名的普通变量。根据“遮盖”规则,从现在开始,在当前目录(子目录)的作用域内${MY_VAR} 将指向这个新的普通变量。

  • message("[子目录] 设置普通变量后: MY_VAR='${MY_VAR}' (普通变量覆盖缓存)")

    • 正如规则所述,它现在读取到的是普通变量的值 "normal_value"

    • 输出[子目录] 设置普通变量后: MY_VAR='normal_value' (普通变量覆盖缓存)

4.显式访问缓存变量

  • message("[子目录] 显式访问缓存: $CACHE{MY_VAR}")

    • CMake 3.13 引入了 $CACHE{VARNAME} 语法,允许你显式地、直接地访问缓存变量的值,绕过任何可能存在的同名普通变量。

    • 所以,这里它成功地读取到了被“遮盖”的缓存变量的原始值 "cache_value"

    • 输出[子目录] 显式访问缓存: cache_value

    • 这个操作非常重要,它证明了缓存变量 MY_VAR 的值自始至终都没有被改变过。

5.返回顶层目录

  • 子目录的 CMakeLists.txt 处理完毕,CMake 返回到顶层目录继续执行。

  • 当离开子目录的作用域时,在那个作用域内定义的普通变量 MY_VAR(值为 "normal_value")就被销毁了

  • message("[顶层] 返回时: MY_VAR='${MY_VAR}' (缓存)")

    • 现在,${MY_VAR} 前面已经没有同名的普通变量来遮盖它了,所以它再次清晰地指向了那个全局的、一直未变的缓存变量

    • 输出[顶层] 返回时: MY_VAR='cache_value' (缓存)

4.2.示例2——普通变量的遮盖效应会传递到子作用域

那我在上一个例子的基础上再加一个 孙子目录,让你清楚看到:

  • 普通变量的遮盖效应会传递到子作用域(子目录、孙子目录),

  • 但是一旦跳出当前作用域,就恢复为缓存变量。


📂 目录结构

demo/
├── CMakeLists.txt
└── sub/
    ├── CMakeLists.txt
    └── subsub/
        └── CMakeLists.txt

🔹 顶层 demo/CMakeLists.txt

cmake_minimum_required(VERSION 3.15)
project(ShadowCacheDemo)

# 定义一个缓存变量
set(MY_VAR "cache_value" CACHE STRING "缓存变量 MY_VAR")

message("[顶层] 初始: MY_VAR='${MY_VAR}' (缓存)")

# 进入子目录
add_subdirectory(sub)

# 回到顶层后,再次读取
message("[顶层] 返回时: MY_VAR='${MY_VAR}' (缓存)")

🔹 子目录 demo/sub/CMakeLists.txt

# 子目录开始
message("[子目录] 进入时: MY_VAR='${MY_VAR}' (继承自缓存)")

# 定义同名普通变量 → 遮盖缓存变量
set(MY_VAR "normal_value")

message("[子目录] 设置普通变量后: MY_VAR='${MY_VAR}' (普通变量覆盖缓存)")
message("[子目录] 显式访问缓存: $CACHE{MY_VAR}")

# 进入孙子目录
add_subdirectory(subsub)

# 回到子目录后
message("[子目录] 返回时: MY_VAR='${MY_VAR}' (普通变量还在遮盖缓存)")

🔹 孙子目录 demo/sub/subsub/CMakeLists.txt

# 孙子目录开始
message("[孙子目录] 进入时: MY_VAR='${MY_VAR}' (继承了子目录的普通变量覆盖)")
message("[孙子目录] 显式访问缓存: $CACHE{MY_VAR}")

我们可以直接复制下面这个代码去一键构建出目录结构和文件

mkdir -p demo/sub/subsub && cat > demo/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.15)
project(ShadowCacheDemo)
set(MY_VAR "cache_value" CACHE STRING "缓存变量 MY_VAR")
message("[顶层] 初始: MY_VAR='${MY_VAR}' (缓存)")
add_subdirectory(sub)
message("[顶层] 返回时: MY_VAR='${MY_VAR}' (缓存)")
EOF

cat > demo/sub/CMakeLists.txt <<'EOF'
message("[子目录] 进入时: MY_VAR='${MY_VAR}' (继承自缓存)")
set(MY_VAR "normal_value")
message("[子目录] 设置普通变量后: MY_VAR='${MY_VAR}' (普通变量覆盖缓存)")
message("[子目录] 显式访问缓存: $CACHE{MY_VAR}")
add_subdirectory(subsub)
message("[子目录] 返回时: MY_VAR='${MY_VAR}' (普通变量还在遮盖缓存)")
EOF

cat > demo/sub/subsub/CMakeLists.txt <<'EOF'
message("[孙子目录] 进入时: MY_VAR='${MY_VAR}' (继承了子目录的普通变量覆盖)")
message("[孙子目录] 显式访问缓存: $CACHE{MY_VAR}")
EOF

接下来我们就来构建这个项目

mkdir build && cd build && cmake ..


✅ 这样你就能清楚看到:

  • 子目录定义的普通变量会传递到孙子目录,继续遮盖缓存变量。

  • 但是回到顶层时,普通变量作用域消失,重新读取的是缓存值。


网站公告

今日签到

点亮在社区的每一天
去签到