CMake新旧方法对比详解

一、include_directories() vs target_include_directories()

这两者的核心区别在于作用域(Scope)

1. include_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 …])

作用域:目录级别

  • 作用: 将指定的目录添加到当前CMakeLists.txt文件以及所有在它之后处理的子目录的头文件搜索路径中
  • 影响: 在调用后定义的所有目标都会将这些目录添加到它们的include路径中
  • 特点: 类似于”全局”设置(在当前目录及子目录范围内)
  • 问题: 不够精确,可能导致目标获得不必要的包含路径

2. target_include_directories(<target> [SYSTEM] [AFTER|BEFORE] <INTERFACE|PUBLIC|PRIVATE> [items1…] [<INTERFACE|PUBLIC|PRIVATE> [items2…] …])

作用域:目标级别

  • 作用: 将指定的目录添加到**特定目标<target>**的头文件搜索路径中
  • 影响: 只有指定的<target>会使用这些include路径
  • 关键修饰符:
    • PRIVATE: 目录只用于编译<target>自身,不会传递给依赖者
    • PUBLIC: 目录既用于编译<target>自身,也会传递给依赖<target>的其他目标
    • INTERFACE: 目录不用于编译<target>自身,但会传递给依赖<target>的其他目标
  • 优点: 非常精确和清晰,为每个目标精确指定所需的头文件路径

对比总结

特性 include_directories() target_include_directories()
作用域 目录级别 (影响当前及子目录所有后续目标) 目标级别 (只影响指定目标)
精确性
封装性 差 (设置是”全局”的) 好 (设置附加到具体目标)
现代推荐 (除非有特定全局需求) (现代CMake的核心用法)
依赖传递 不直接处理 通过PUBLIC/INTERFACE关键字明确控制

二、CMake旧方式 vs 新方式(现代CMake)

核心区别在于从基于变量和全局/目录设置转向基于目标及其属性

1. 旧方式 (Variable-Centric, Directory-Scoped - CMake 2.x时代)

核心思想: find_package等命令设置全局变量,手动使用这些变量配置目标

典型流程:

  1. 查找和配置库

    1
    2
    3
    find_package(SomeLib REQUIRED)
    # 执行FindSomeLib.cmake或旧式SomeLibConfig.cmake
    # 设置变量:SomeLib_FOUND, SomeLib_INCLUDE_DIRS, SomeLib_LIBRARIES等
  2. 手动应用这些变量

    1
    2
    3
    4
    5
    6
    7
    # 全局设置,影响后续所有目标
    include_directories(${SomeLib_INCLUDE_DIRS})
    add_definitions(${SomeLib_DEFINITIONS})

    add_executable(my_app main.cpp)
    # 手动链接库
    target_link_libraries(my_app ${SomeLib_LIBRARIES})

特点与问题:

  • 全局状态: 变量和设置通常影响整个目录或项目,容易冲突
  • 手动管理: 需了解每个库设置的变量,手动应用到目标的各属性
  • 传递依赖困难:my_app依赖my_lib,而my_lib依赖SomeLib,则my_app的配置文件可能也需了解SomeLib,破坏封装性
  • 不够清晰: 链接指令通常只是变量引用,不明确表达依赖关系

2. 新方式 (Modern CMake / Target-Centric - CMake 3.0+时代)

核心思想: 将构建所需的所有信息附加到目标上,使用导入目标(Imported Targets)

典型流程:

  1. 查找和配置库

    1
    2
    3
    find_package(SomeLib REQUIRED)
    # 执行现代SomeLibConfig.cmake
    # 定义导入目标,如SomeLib::Core
  2. 直接链接导入目标

    1
    2
    3
    4
    add_executable(my_app main.cpp)

    # 只需链接目标,CMake自动处理其他所有事情
    target_link_libraries(my_app PRIVATE SomeLib::Core)

特点与优势:

  • 目标即一切: 所有配置围绕目标进行,使用target_*系列命令
  • 封装性: 库的使用细节被封装在导入目标中,配置文件只需知道目标名称
  • 自动传递依赖: 链接导入目标时,该目标的公共依赖自动传递
  • 清晰明确: 链接指令直接表明依赖关系
  • 精确控制: 使用PRIVATE/PUBLIC/INTERFACE关键字精确控制依赖传递

对比总结

方面 旧方式 (Variable-Centric) 新方式 (Target-Centric)
核心 全局/目录变量 (_DIRS, _LIBS) 目标及其属性 (target_*命令, 导入目标)
find_package 主要设置变量 主要定义导入目标 (Namespace::Target)
配置方式 手动应用变量到目标 链接导入目标,CMake自动处理细节
包含路径 include_directories() (全局) target_include_directories() (目标级)
链接 target_link_libraries(... ${..._LIBS}) target_link_libraries(... Namespace::Target)
依赖传递 手动处理,易出错 自动处理 (通过PUBLIC/INTERFACE)
封装性
推荐度 不推荐 强烈推荐

三、实践建议

  1. 始终优先使用现代CMake方法

    • 使用target_*系列命令而非全局设置
    • 链接导入目标而非变量列表
  2. 合理使用作用域关键字

    • PRIVATE: 仅在目标内部使用,不传递给依赖者
    • PUBLIC: 在目标内部使用且传递给依赖者
    • INTERFACE: 不在目标内部使用,仅传递给依赖者
  3. 创建自己的库时

    • 导出明确的目标而非变量
    • 正确设置PUBLICINTERFACE属性以确保依赖正确传递
  4. 处理旧式库时

    • 可以创建接口库封装旧式变量,使其符合现代CMake风格
    1
    2
    3
    4
    5
    add_library(SomeOldLib::SomeOldLib INTERFACE IMPORTED)
    set_target_properties(SomeOldLib::SomeOldLib PROPERTIES
    INTERFACE_INCLUDE_DIRECTORIES "${SomeOldLib_INCLUDE_DIRS}"
    INTERFACE_LINK_LIBRARIES "${SomeOldLib_LIBRARIES}"
    )