29、最佳实践:现代CMake推荐写法、避免的陷阱、代码风格指南、项目模板推荐

说实话,CMake 这东西,入门容易,写好难。我见过太多项目,CMakeLists.txt 写得跟天书似的,几百行下来,想改个编译选项都得找半天。今天我就把这几年的实战经验掰开揉碎,跟你聊聊现代 CMake 到底该怎么写。

核心思想:现代 CMake 不是写脚本,而是描述依赖关系。你告诉 CMake「我要什么」,而不是「你怎么做」。

一、现代 CMake 推荐写法

1.1 版本声明要明确

每个 CMakeLists.txt 的第一行,我建议你写上版本声明。为什么?因为不同版本的 CMake 行为差异很大。我踩过这个坑——3.10 和 3.20 对 target_include_directories 的处理方式就不一样。

cmake_minimum_required(VERSION 3.16...3.28)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)

这里用了范围语法,意思是「最低 3.16,最高 3.28」。这样既保证了向下兼容,又能用上新特性。

1.2 用 target 思维,别用全局变量

老式 CMake 喜欢这么写:

# ❌ 不推荐
include_directories(${PROJECT_SOURCE_DIR}/include)
add_definitions(-DDEBUG)
link_directories(${PROJECT_SOURCE_DIR}/lib)

现代写法是这样的:

# ✅ 推荐
add_library(mylib src/mylib.cpp)
target_include_directories(mylib PUBLIC include)
target_compile_definitions(mylib PRIVATE DEBUG)
target_link_libraries(myapp PRIVATE mylib)

你看,每个 target 的依赖关系清清楚楚。PUBLIC、PRIVATE、INTERFACE 这三个关键字,说白了就是「谁能用这个属性」。我个人习惯:能用 PRIVATE 绝不用 PUBLIC,避免接口泄漏。

1.3 生成器表达式是你的好朋友

很多人在 CMake 里写 if-else 判断平台,其实有更优雅的方式:

target_compile_options(mylib PRIVATE
  $<$<CXX_COMPILER_ID:MSVC>:/W4>
  $<$<CXX_COMPILER_ID:GNU>:-Wall -Wextra>
)

target_link_libraries(myapp PRIVATE
  $<$<PLATFORM_ID:Windows>:ws2_32>
  $<$<PLATFORM_ID:Linux>:pthread>
)

生成器表达式在生成构建系统时才求值,比 if 块更灵活。嗯,这里要注意:表达式里不能有空格,否则会解析失败。

二、避免的陷阱

陷阱一:滥用全局变量

我曾经接手过一个项目,CMakeLists.txt 里定义了 30 多个全局变量,什么 LIB_PATH、INC_DIR、EXTRA_FLAGS…… 改一个变量,好几个地方跟着崩。现代 CMake 里,全局变量应该只用于项目级别的配置,比如安装路径、版本号。其他东西,都绑到 target 上。

陷阱二:忘记设置 C++ 标准

很多人以为编译器默认就是 C++17,结果换台机器就编译不过。正确的做法:

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

第三行 CMAKE_CXX_EXTENSIONS OFF 很多人会漏掉。它关掉了 GCC 的扩展语法,保证代码的可移植性。

陷阱三:硬编码路径

「在我电脑上能跑啊」—— 这句话我听了太多次了。别把 /usr/local 或者 C:\Qt\5.15 写死在 CMake 里。用 find_package 或者 FetchContent 来管理依赖。

三、代码风格指南

风格这东西,没有绝对的对错。但团队协作时,统一风格能省下大量沟通成本。下面是我个人推荐的一套规则:

规则 推荐写法 不推荐写法
命令大小写 add_library 小写 ADD_LIBRARY 大写
变量命名 MY_PROJECT_SRC 全大写 myProjectSrc 驼峰
target 命名 my_library 小写加下划线 MyLibrary 驼峰
缩进 2 空格 Tab 或 4 空格
空行 函数之间 1 空行 连续多个空行

你想想看,如果团队里有人用 Tab 有人用空格,那 git diff 看起来得多痛苦。我建议直接在项目根目录放一个 .cmake-format 配置文件,用工具自动格式化。

四、项目模板推荐

说了这么多理论,不如直接给个模板。这是我个人项目里常用的结构:

my_project/
├── CMakeLists.txt          # 顶层 CMake
├── cmake/
│   ├── CompilerWarnings.cmake  # 编译器警告配置
│   ├── StandardSettings.cmake  # C++ 标准、构建类型
│   └── Dependencies.cmake      # 依赖管理
├── src/
│   ├── CMakeLists.txt
│   └── main.cpp
├── libs/
│   ├── core/
│   │   ├── CMakeLists.txt
│   │   └── core.cpp
│   └── utils/
│       ├── CMakeLists.txt
│       └── utils.cpp
├── tests/
│   ├── CMakeLists.txt
│   └── test_core.cpp
└── extern/
    └── CMakeLists.txt      # FetchContent 管理第三方库

顶层 CMakeLists.txt 长这样:

cmake_minimum_required(VERSION 3.16...3.28)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)

# 引入模块化的 cmake 文件
include(cmake/StandardSettings.cmake)
include(cmake/CompilerWarnings.cmake)
include(cmake/Dependencies.cmake)

# 添加子目录
add_subdirectory(libs/core)
add_subdirectory(libs/utils)
add_subdirectory(src)

# 测试可选
option(BUILD_TESTS "Build tests" ON)
if(BUILD_TESTS)
  enable_testing()
  add_subdirectory(tests)
endif()

这种结构的好处是:每个模块的 CMake 文件都很短,职责单一。想改编译器警告?去 CompilerWarnings.cmake 里改。想加个新库?去 Dependencies.cmake 里加。清爽得很。

小技巧:Dependencies.cmake 里,我习惯用 FetchContent 来管理所有第三方库。这样只要一个 cmake .. 就能把依赖全部拉下来,不用手动安装。对于公司内部库,可以用 find_package 配合 CMAKE_PREFIX_PATH

五、知识体系总览

下面这张图,是我对本章内容的总结。你可以把它当作一个检查清单:

现代 CMake 最佳实践 推荐写法 • 版本声明明确 • target 思维 • 生成器表达式 • 模块化 cmake 文件 避免陷阱 • 滥用全局变量 • 忘记 C++ 标准 • 硬编码路径 • 忽略可移植性 风格指南 • 命令小写 • 变量全大写 • target 下划线 • 2 空格缩进 项目模板 • 顶层 CMakeLists.txt • cmake/ 模块目录 • src/ libs/ tests/ 分离 辅助工具 • cmake-format 格式化 • FetchContent 依赖管理 • CPack 打包

这张图把本章的核心内容串起来了。你写 CMake 的时候,可以对照着检查:写法对不对?有没有踩坑?风格统一吗?模板结构合理吗?

最后说一句:CMake 的学习曲线确实陡,但一旦掌握了现代写法,你会发现它其实很优雅。别怕犯错,我踩过的坑比你多得多。关键是每次踩坑后,把经验沉淀下来,变成团队的最佳实践。


公众号:蓝海资料掘金营,微信deep3321