强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

CMake 从入门到精通:完整教程 / 第 18 章:最佳实践

第 18 章:最佳实践

18.1 现代 CMake 原则

18.1.1 以目标为中心

# ❌ 旧风格:全局设置
include_directories(include/)
add_definitions(-DDEBUG)
link_libraries(mylib)

# ✅ 现代风格:目标级别设置
target_include_directories(myapp PRIVATE include/)
target_compile_definitions(myapp PRIVATE DEBUG)
target_link_libraries(myapp PRIVATE mylib)

18.1.2 使用 IMPORTED 目标

# ❌ 变量方式
find_package(OpenSSL)
include_directories(${OPENSSL_INCLUDE_DIR})
link_libraries(${OPENSSL_LIBRARIES})

# ✅ 导入目标方式
find_package(OpenSSL REQUIRED)
target_link_libraries(myapp PRIVATE OpenSSL::SSL)

18.1.3 最小权限原则

# ✅ 仅自己使用
target_link_libraries(mylib PRIVATE fmt::fmt)

# ✅ 需要传递给使用者
target_link_libraries(mylib PUBLIC OpenSSL::SSL)

# ✅ 仅使用者需要(纯头文件库)
target_link_libraries(mylib INTERFACE range-v3::range-v3)

18.1.4 现代 CMake 规则对照表

场景旧风格(避免)现代风格(推荐)
包含目录include_directories()target_include_directories()
编译定义add_definitions()target_compile_definitions()
编译选项add_compile_options()target_compile_options()
链接库link_libraries()target_link_libraries()
C++ 标准CMAKE_CXX_STANDARDtarget_compile_features()
查找库变量 ${XXX_LIBRARIES}目标 XXX::XXX

18.2 项目结构

18.2.1 推荐目录结构

myproject/
├── CMakeLists.txt              # 顶层构建文件
├── CMakePresets.json           # 预设配置
├── cmake/                      # CMake 模块
│   ├── FindMyLib.cmake
│   ├── CompilerWarnings.cmake
│   ├── Sanitizers.cmake
│   ├── MyProjectConfig.cmake.in
│   └── Version.cmake
├── include/                    # 公共头文件
│   └── myproject/
│       ├── core.h
│       ├── utils.h
│       └── version.h
├── src/                        # 源代码
│   ├── CMakeLists.txt
│   ├── core.cpp
│   └── utils.cpp
├── tests/                      # 测试
│   ├── CMakeLists.txt
│   ├── test_core.cpp
│   └── test_utils.cpp
├── examples/                   # 示例
│   ├── CMakeLists.txt
│   └── example1.cpp
├── docs/                       # 文档
│   ├── CMakeLists.txt
│   └── Doxyfile.in
├── third_party/                # 第三方库(可选)
│   └── CMakeLists.txt
└── scripts/                    # 脚本
    ├── build.sh
    └── ci.sh

18.2.2 顶层 CMakeLists.txt 模板

cmake_minimum_required(VERSION 3.16...3.28)

project(MyProject
    VERSION 1.0.0
    DESCRIPTION "项目描述"
    LANGUAGES CXX C
)

# 仅在主项目中启用(非被依赖时)
if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
    # 默认构建类型
    if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
        set(CMAKE_BUILD_TYPE Release CACHE STRING "构建类型" FORCE)
        set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
            Debug Release RelWithDebInfo MinSizeRel)
    endif()

    # 测试
    option(BUILD_TESTING "构建测试" ON)
    include(CTest)
endif()

# 模块路径
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")

# 编译选项
add_library(project_warnings INTERFACE)
include(CompilerWarnings)

add_library(project_options INTERFACE)
target_compile_features(project_options INTERFACE cxx_std_17)

# 子目录
add_subdirectory(src)

if(BUILD_TESTING)
    add_subdirectory(tests)
endif()

option(BUILD_EXAMPLES "构建示例" OFF)
if(BUILD_EXAMPLES)
    add_subdirectory(examples)
endif()

# 安装和打包
include(GNUInstallDirs)
include(CMakePackageConfigHelpers)
include(CPack)

18.2.3 src/CMakeLists.txt

add_library(myproject_core
    core.cpp
    utils.cpp
)

target_include_directories(myproject_core
    PUBLIC
        $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
        $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)

target_link_libraries(myproject_core
    PUBLIC project_options
    PRIVATE project_warnings
)

# 设置输出名
set_target_properties(myproject_core PROPERTIES
    OUTPUT_NAME "myproject"
    VERSION ${PROJECT_VERSION}
    SOVERSION ${PROJECT_VERSION_MAJOR}
    POSITION_INDEPENDENT_CODE ON
)

# 别名
add_library(MyProject::Core ALIAS myproject_core)

# 安装
install(TARGETS myproject_core
    EXPORT MyProjectTargets
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)

18.3 代码组织最佳实践

18.3.1 编译器警告配置

# cmake/CompilerWarnings.cmake
function(myproject_set_warnings target)
    option(WARNINGS_AS_ERRORS "将警告视为错误" OFF)

    set(MSVC_WARNINGS /W4 /WX /wd4251 /wd4275)
    set(CLANG_WARNINGS -Wall -Wextra -Wpedantic -Wshadow -Wconversion)
    set(GCC_WARNINGS ${CLANG_WARNINGS} -Wmisleading-indentation -Wduplicated-cond)

    if(WARNINGS_AS_ERRORS)
        list(APPEND CLANG_WARNINGS -Werror)
        list(APPEND GCC_WARNINGS -Werror)
        list(APPEND MSVC_WARNINGS /WX)
    endif()

    target_compile_options(${target} INTERFACE
        $<$<CXX_COMPILER_ID:MSVC>:${MSVC_WARNINGS}>
        $<$<CXX_COMPILER_ID:Clang>:${CLANG_WARNINGS}>
        $<$<CXX_COMPILER_ID:GNU>:${GCC_WARNINGS}>
    )
endfunction()

myproject_set_warnings(project_warnings)

18.3.2 Sanitizer 配置

# cmake/Sanitizers.cmake
option(ENABLE_ADDRESS_SANITIZER "启用 ASan" OFF)
option(ENABLE_THREAD_SANITIZER "启用 TSan" OFF)
option(ENABLE_UNDEFINED_SANITIZER "启用 UBSan" OFF)

function(myproject_enable_sanitizers target)
    if(ENABLE_ADDRESS_SANITIZER)
        target_compile_options(${target} INTERFACE
            -fsanitize=address -fno-omit-frame-pointer
        )
        target_link_options(${target} INTERFACE -fsanitize=address)
    endif()

    if(ENABLE_THREAD_SANITIZER)
        if(ENABLE_ADDRESS_SANITIZER)
            message(FATAL_ERROR "ASan 和 TSan 不能同时启用")
        endif()
        target_compile_options(${target} INTERFACE -fsanitize=thread)
        target_link_options(${target} INTERFACE -fsanitize=thread)
    endif()

    if(ENABLE_UNDEFINED_SANITIZER)
        target_compile_options(${target} INTERFACE -fsanitize=undefined)
        target_link_options(${target} INTERFACE -fsanitize=undefined)
    endif()
endfunction()

add_library(sanitizers INTERFACE)
myproject_enable_sanitizers(sanitizers)

18.3.3 版本管理

# cmake/Version.cmake
# 从 git 获取版本信息
function(myproject_get_version)
    find_package(Git QUIET)
    if(GIT_FOUND)
        execute_process(
            COMMAND ${GIT_EXECUTABLE} describe --tags --always
            WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
            OUTPUT_VARIABLE GIT_VERSION
            OUTPUT_STRIP_TRAILING_WHITESPACE
            ERROR_QUIET
        )
        execute_process(
            COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD
            WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
            OUTPUT_VARIABLE GIT_HASH
            OUTPUT_STRIP_TRAILING_WHITESPACE
            ERROR_QUIET
        )
    endif()

    set(PROJECT_GIT_VERSION "${GIT_VERSION}" PARENT_SCOPE)
    set(PROJECT_GIT_HASH "${GIT_HASH}" PARENT_SCOPE)
endfunction()

myproject_get_version()
message(STATUS "Git 版本: ${PROJECT_GIT_VERSION}")
message(STATUS "Git 哈希: ${PROJECT_GIT_HASH}")
// version.h.in
#pragma once
#define MYPROJECT_VERSION "@PROJECT_VERSION@"
#define MYPROJECT_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define MYPROJECT_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define MYPROJECT_VERSION_PATCH @PROJECT_VERSION_PATCH@
#define MYPROJECT_GIT_VERSION "@PROJECT_GIT_VERSION@"
#define MYPROJECT_GIT_HASH "@PROJECT_GIT_HASH@"

18.4 性能优化

18.4.1 减少配置时间

# 1. 避免 file(GLOB)
# ❌
file(GLOB_RECURSE SOURCES "src/*.cpp")
# ✅
set(SOURCES src/a.cpp src/b.cpp src/c.cpp)

# 2. 减少 try_compile 调用
# 3. 使用预编译头
target_precompile_headers(mylib PRIVATE
    <vector>
    <string>
    <memory>
    <algorithm>
)

# 4. 并行配置
# 无直接支持,但可以优化 CMakeLists.txt

18.4.2 减少构建时间

# 1. Unity Build(合并编译单元)
set_target_properties(mylib PROPERTIES UNITY_BUILD ON)

# 2. 预编译头
target_precompile_headers(mylib PRIVATE "pch.h")

# 3. ccache 集成
find_program(CCACHE_PROGRAM ccache)
if(CCACHE_PROGRAM)
    set(CMAKE_C_COMPILER_LAUNCHER ${CCACHE_PROGRAM})
    set(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE_PROGRAM})
endif()

# 4. 对象库避免重复编译
add_library(backend_objects OBJECT src/backend.cpp)
add_library(backend_static STATIC $<TARGET_OBJECTS:backend_objects>)
add_library(backend_shared SHARED $<TARGET_OBJECTS:backend_objects>)

# 5. 排除非必要目标
add_subdirectory(tests EXCLUDE_FROM_ALL)

18.4.3 减少安装体积

# 1. 去除调试信息
install(CODE "execute_process(COMMAND ${CMAKE_STRIP} \$<TARGET_FILE:myapp>)")

# 2. LTO(链接时优化)
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE)

# 3. 按组件安装
install(TARGETS myapp
    RUNTIME DESTINATION bin COMPONENT Runtime
)
install(TARGETS mylib
    ARCHIVE DESTINATION lib COMPONENT Development
    LIBRARY DESTINATION lib COMPONENT Runtime
)

# 4. 分离调试符号
set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE)
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)

18.5 可维护性

18.5.1 清晰的依赖关系

# 每个模块明确声明自己的依赖
add_library(core src/core.cpp)
target_link_libraries(core PUBLIC base)

add_library(network src/network.cpp)
target_link_libraries(network PUBLIC core OpenSSL::SSL)

add_library(database src/database.cpp)
target_link_libraries(database PUBLIC core PostgreSQL::PostgreSQL)

# 应用程序只需链接需要的模块
add_executable(app main.cpp)
target_link_libraries(app PRIVATE network database)

18.5.2 避免全局状态

# ❌ 全局设置影响所有目标
set(CMAKE_CXX_FLAGS "-Wall -Wextra")
add_definitions(-DDEBUG)
include_directories(include/)

# ✅ 使用接口库封装配置
add_library(project_defaults INTERFACE)
target_compile_options(project_defaults INTERFACE
    $<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra>
    $<$<CXX_COMPILER_ID:MSVC>:/W4>
)
target_include_directories(project_defaults INTERFACE include/)
target_compile_features(project_defaults INTERFACE cxx_std_17)

# 使用时
target_link_libraries(mylib PUBLIC project_defaults)

18.5.3 文档化 CMakeLists.txt

# 模块说明
#[=======================================================================[.rst:
MyModule
--------

提供项目的通用配置。

功能
^^^^

- 设置编译器警告
- 配置 Sanitizer
- 定义版本信息

使用方法
^^^^^^^^

.. code-block:: cmake

    include(MyModule)
    target_link_libraries(myapp PRIVATE project_defaults)
#]=======================================================================]

18.5.4 版本控制

# .gitignore
build/
build-*/
.cache/
CMakeUserPresets.json
*.user
compile_commands.json
// .vscode/settings.json
{
    "cmake.buildDirectory": "${workspaceFolder}/build",
    "cmake.configureOnOpen": true,
    "cmake.configureSettings": {
        "CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
    }
}

18.6 测试最佳实践

18.6.1 测试组织

# tests/CMakeLists.txt
enable_testing()

# 测试目标
add_executable(test_core test_core.cpp)
target_link_libraries(test_core PRIVATE
    MyProject::Core
    GTest::gtest_main
)

# 自动发现测试
include(GoogleTest)
gtest_discover_tests(test_core
    PROPERTIES
        LABELS "unit"
        TIMEOUT 30
)

18.6.2 测试分类

# 按类型组织测试
add_subdirectory(unit)         # 单元测试
add_subdirectory(integration)  # 集成测试
add_subdirectory(performance)  # 性能测试

# CTest 过滤
# ctest -L unit                 # 运行单元测试
# ctest -L integration          # 运行集成测试
# ctest -LE "slow|performance"  # 排除慢测试

18.7 安全最佳实践

18.7.1 编译安全选项

# 安全编译选项
target_compile_options(myapp PRIVATE
    # 堆栈保护
    -fstack-protector-strong
    # 格式化字符串保护
    -Wformat -Wformat-security -Werror=format-security
    # 位置无关代码
    -fPIC
    # 立即绑定
    -Wl,-z,now
    # 不可执行栈
    -Wl,-z,noexecstack
)

# 安全定义
target_compile_definitions(myapp PRIVATE
    _FORTIFY_SOURCE=2
)

18.7.2 依赖安全

# 锁定依赖版本
FetchContent_Declare(fmt
    GIT_REPOSITORY https://github.com/fmtlib/fmt.git
    GIT_TAG 10.2.1    # 明确版本
    GIT_SHALLOW TRUE
)

# 验证下载哈希
FetchContent_Declare(mylib
    URL https://example.com/mylib-2.0.tar.gz
    URL_HASH SHA256=abc123...
)

18.8 跨平台最佳实践

18.8.1 平台抽象

# 平台特定源文件
add_library(mylib
    src/common.cpp
    $<$<PLATFORM_ID:Linux>:src/linux.cpp>
    $<$<PLATFORM_ID:Windows>:src/windows.cpp>
    $<$<PLATFORM_ID:Darwin>:src/macos.cpp>
)

# 平台特定链接
target_link_libraries(mylib PRIVATE
    $<$<PLATFORM_ID:Linux>:rt;dl>
    $<$<PLATFORM_ID:Windows>:ws2_32;bcrypt>
    $<$<PLATFORM_ID:Darwin>:"-framework CoreFoundation">
)

18.8.2 编译器抽象

# 编译器特定选项
target_compile_options(mylib PRIVATE
    $<$<CXX_COMPILER_ID:GNU>:
        -Wall -Wextra -Wpedantic -Wshadow -Wconversion
        $<$<VERSION_GREATER_EQUAL:${CMAKE_CXX_COMPILER_VERSION},10>:-Wconversion>
    >
    $<$<CXX_COMPILER_ID:Clang>:
        -Wall -Wextra -Wpedantic -Wshadow -Wconversion
    >
    $<$<CXX_COMPILER_ID:MSVC>:
        /W4 /WX /wd4251 /wd4275 /wd4996
    >
)

18.9 CMakePresets.json 标准配置

{
    "version": 6,
    "cmakeMinimumRequired": { "major": 3, "minor": 21 },
    "configurePresets": [
        {
            "name": "base",
            "hidden": true,
            "generator": "Ninja",
            "binaryDir": "${sourceDir}/build/${presetName}",
            "cacheVariables": {
                "CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
                "BUILD_TESTING": "ON"
            }
        },
        {
            "name": "debug",
            "displayName": "Debug",
            "inherits": "base",
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Debug"
            }
        },
        {
            "name": "release",
            "displayName": "Release",
            "inherits": "base",
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Release"
            }
        },
        {
            "name": "ci",
            "displayName": "CI",
            "inherits": "release",
            "cacheVariables": {
                "WARNINGS_AS_ERRORS": "ON"
            },
            "condition": {
                "type": "equals",
                "lhs": "$env{CI}",
                "rhs": "true"
            }
        }
    ],
    "buildPresets": [
        { "name": "debug", "configurePreset": "debug" },
        { "name": "release", "configurePreset": "release" }
    ],
    "testPresets": [
        {
            "name": "quick",
            "configurePreset": "debug",
            "filter": { "include": { "label": "fast" } }
        },
        {
            "name": "full",
            "configurePreset": "debug",
            "output": { "outputOnFailure": true }
        }
    ]
}

18.10 最佳实践清单

✅ 项目设置

  • 使用 cmake_minimum_required(VERSION 3.16...3.28)
  • 使用 out-of-source 构建
  • 设置 CMAKE_EXPORT_COMPILE_COMMANDS
  • 提供 CMakePresets.json
  • CMakeUserPresets.json 加入 .gitignore

✅ 目标管理

  • 以目标为中心设置属性
  • 正确使用 PRIVATE/PUBLIC/INTERFACE
  • 使用命名空间别名(MyProject::Core
  • 为库设置 SOVERSION

✅ 依赖管理

  • 优先使用 find_package 的 IMPORTED 目标
  • FetchContent 使用明确的 tag/hash
  • 考虑 FIND_PACKAGE_ARGS 的优先级

✅ 编译配置

  • 使用 target_compile_features() 设置标准
  • 设置 CXX_EXTENSIONS OFF
  • 配置编译器警告
  • 支持 Sanitizer

✅ 安装与打包

  • 使用 GNUInstallDirs
  • 生成并安装包配置文件
  • 设置 RPATH
  • 提供 CPack 配置

✅ 测试

  • 集成测试框架
  • 使用 gtest_discover_tests()
  • 设置测试标签
  • 配置覆盖率

✅ 可维护性

  • 清晰的目录结构
  • 模块化的 CMakeLists.txt
  • 避免全局状态
  • 文档化复杂的 CMake 逻辑

18.11 扩展阅读


上一章:第 17 章 — 问题排查 | 返回目录:目录