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 的时候,可以对照着检查:写法对不对?有没有踩坑?风格统一吗?模板结构合理吗?
最后说一句:CMake 的学习曲线确实陡,但一旦掌握了现代写法,你会发现它其实很优雅。别怕犯错,我踩过的坑比你多得多。关键是每次踩坑后,把经验沉淀下来,变成团队的最佳实践。
公众号:蓝海资料掘金营,微信deep3321