3、变量与缓存:CMake变量定义与引用、set()命令、缓存变量、CMake预设变量、变量作用域
好,咱们今天来聊聊CMake里的变量。说实话,变量这东西,看着简单,但用不好真的会掉坑里。我见过不少项目,CMakeLists.txt写得跟天书似的,就是因为变量管理混乱。这一节,咱们就把变量这事儿彻底捋清楚。
3.1 变量的定义与引用:最基础的操作
在CMake里,变量用set()命令来定义。引用变量时,用${变量名}。这跟Shell脚本有点像,但又不完全一样。
# 定义一个字符串变量
set(MY_PROJECT_NAME "AwesomeApp")
# 定义一个列表变量
set(MY_SOURCES main.cpp utils.cpp network.cpp)
# 引用变量
message("项目名称: ${MY_PROJECT_NAME}")
message("源文件: ${MY_SOURCES}")
这里有个细节,set()命令可以接受多个值,它们会自动组成一个列表。说白了,就是分号分隔的字符串。我个人习惯用引号把值包起来,这样更清晰,也能避免一些奇怪的解析问题。
if(DEFINED ...)检查一下。
3.2 set()命令的进阶用法
set()命令远不止定义变量这么简单。它有几个重要的选项,咱们一个一个看。
3.2.1 设置缓存变量:CACHE
缓存变量是CMake里一个非常重要的概念。它允许用户在命令行通过-D选项修改变量的值,而且这个值会被缓存下来,下次构建时仍然有效。
# 定义一个缓存变量,默认值为OFF
set(USE_OPENCV OFF CACHE BOOL "是否启用OpenCV支持")
# 用户可以在命令行这样修改:
# cmake -DUSE_OPENCV=ON ..
嗯,这里要注意,CACHE后面的BOOL是变量类型,CMake支持BOOL、FILEPATH、PATH、STRING、INTERNAL这几种类型。其中INTERNAL类型不会出现在cmake-gui中,适合内部使用的缓存变量。
set()命令覆盖(除非加FORCE选项)。我曾经因为这个特性,调试了半天才发现是缓存变量在作祟。所以,修改缓存变量时,记得清理CMakeCache.txt或者用-D重新设置。
3.2.2 强制覆盖:FORCE
如果你确实需要覆盖一个已有的缓存变量,可以用FORCE选项:
set(USE_OPENCV ON CACHE BOOL "是否启用OpenCV支持" FORCE)
但说实话,我很少用FORCE。因为它破坏了缓存变量的设计初衷。除非你在写一个很底层的工具链文件,否则尽量避免。
3.3 CMake预设变量:那些你不需要定义就能用的变量
CMake内置了很多预设变量,它们提供了编译器、平台、项目等关键信息。用好这些变量,能让你的CMakeLists.txt更简洁、更跨平台。
| 变量名 | 说明 | 示例值 |
|---|---|---|
CMAKE_CXX_STANDARD |
C++标准版本 | 17, 20 |
CMAKE_CXX_STANDARD_REQUIRED |
是否强制要求指定的标准 | ON, OFF |
CMAKE_CXX_EXTENSIONS |
是否允许编译器扩展 | ON, OFF |
CMAKE_BUILD_TYPE |
构建类型 | Debug, Release, RelWithDebInfo, MinSizeRel |
CMAKE_SOURCE_DIR |
顶层CMakeLists.txt所在目录 | /home/user/project |
CMAKE_BINARY_DIR |
构建目录 | /home/user/project/build |
CMAKE_CURRENT_SOURCE_DIR |
当前处理的CMakeLists.txt所在目录 | /home/user/project/src |
CMAKE_CURRENT_BINARY_DIR |
当前处理的CMakeLists.txt对应的构建目录 | /home/user/project/build/src |
PROJECT_NAME |
当前project()命令定义的项目名 | MyProject |
PROJECT_SOURCE_DIR |
当前project()命令对应的源码目录 | /home/user/project |
PROJECT_BINARY_DIR |
当前project()命令对应的构建目录 | /home/user/project/build |
举个例子,设置C++17标准:
# 设置C++标准为17,并且强制要求
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF) # 关闭编译器扩展,保证可移植性
为什么我要强调CMAKE_CXX_EXTENSIONS OFF?因为不同编译器的扩展行为不一样,你想想看,如果用了GCC的扩展语法,换到MSVC上可能就编译不过了。我在跨平台项目里吃过这个亏,所以现在都习惯关掉它。
3.4 变量作用域:别让你的变量“串门”
CMake的变量作用域跟C++的块作用域有点像,但又有自己的特点。理解作用域,是写出健壮CMakeLists.txt的关键。
3.4.1 目录作用域
每个add_subdirectory()调用会创建一个新的作用域。子目录中的变量修改不会影响父目录,除非使用set(... PARENT_SCOPE)。
# 顶层CMakeLists.txt
set(MY_VAR "hello")
add_subdirectory(subdir)
message("顶层: ${MY_VAR}") # 输出 "hello"
# subdir/CMakeLists.txt
set(MY_VAR "world")
message("子目录: ${MY_VAR}") # 输出 "world"
# 如果想修改父目录的变量:
set(MY_VAR "world" PARENT_SCOPE)
嗯,这里要注意,PARENT_SCOPE只能向上传递一层。如果你有多层嵌套,需要逐层传递。我个人建议,尽量避免这种复杂的变量传递,用缓存变量或者全局属性来代替。
3.4.2 函数作用域
函数function()也会创建新的作用域。函数内部的变量修改,默认不会影响外部。
function(my_function)
set(MY_VAR "inside")
message("函数内: ${MY_VAR}") # 输出 "inside"
endfunction()
set(MY_VAR "outside")
my_function()
message("函数外: ${MY_VAR}") # 输出 "outside"
如果想从函数里修改外部变量,同样可以用PARENT_SCOPE,或者用set(... CACHE ... FORCE)。但说实话,我更推荐用返回值的方式,比如通过set(... PARENT_SCOPE)把结果“传”出去。
3.4.3 缓存变量的全局性
缓存变量是全局的,不受作用域限制。这也是为什么我建议用缓存变量来传递跨目录的配置信息。
# 在任何地方都可以访问和修改缓存变量
set(MY_GLOBAL_VAR "global" CACHE STRING "全局变量")
3.5 实战:一个完整的变量使用示例
咱们把今天学的知识串起来,写一个实际点的例子。
cmake_minimum_required(VERSION 3.15)
project(VariableDemo VERSION 1.0.0 LANGUAGES CXX)
# 1. 设置C++标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# 2. 定义项目级别的缓存变量
set(ENABLE_TESTS ON CACHE BOOL "是否启用测试")
set(ENABLE_LOGGING OFF CACHE BOOL "是否启用日志")
# 3. 根据缓存变量设置编译选项
if(ENABLE_LOGGING)
add_compile_definitions(ENABLE_LOGGING)
endif()
# 4. 添加可执行文件
add_executable(${PROJECT_NAME} main.cpp)
# 5. 添加子目录
add_subdirectory(src)
# 6. 根据缓存变量决定是否添加测试
if(ENABLE_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
这个例子涵盖了今天讲的大部分内容:预设变量、缓存变量、作用域、条件判断。你想想看,如果不用缓存变量,用户怎么在命令行控制是否启用测试?只能手动改CMakeLists.txt,那多麻烦。
- 普通变量用
set()定义,用${}引用 - 缓存变量用
set(... CACHE ...)定义,用户可通过-D修改 - 预设变量提供了编译器、平台、项目等关键信息
- 变量作用域分为目录作用域、函数作用域和缓存变量的全局作用域
- 用
PARENT_SCOPE可以向上传递变量,但尽量少用
好了,变量这块儿就讲这么多。下一节咱们聊聊CMake的列表和字符串操作,那才是真正体现CMake“脚本语言”特性的地方。到时候我会分享一些我在实际项目中用到的骚操作,保证让你大开眼界。