12、生成器表达式实战:条件编译、平台特定源文件选择、调试与发布不同链接库、安装路径动态计算

生成器表达式(Generator Expressions)是 CMake 里最强大的特性之一。说实话,我刚开始接触 CMake 时,觉得这东西有点绕——不就是个字符串处理吗?后来在项目中踩了几次坑,才真正体会到它的价值。

生成器表达式在构建系统生成阶段才被求值。这意味着你可以根据配置、平台、编译器等信息,动态决定编译什么、链接什么、安装到哪里。说白了,它让 CMakeLists.txt 变得「智能」了。

12.1 条件编译:用 $<CONFIG> 控制代码路径

条件编译是最常见的需求。同一个源文件,在 Debug 和 Release 下可能需要不同的宏定义。

target_compile_definitions(myapp PRIVATE
    $<$<CONFIG:Debug>:_DEBUG>
    $<$<CONFIG:Release>:NDEBUG>
)

这段代码的意思是:如果当前是 Debug 配置,就定义 _DEBUG;如果是 Release,就定义 NDEBUG。生成器表达式用嵌套的 $<...> 实现条件判断。

我的习惯: 我一般把条件编译的生成器表达式单独写一行,方便阅读。别把它们挤在一行里,否则维护起来很痛苦。

你还可以组合多个条件:

target_compile_definitions(myapp PRIVATE
    $<$<AND:$<CONFIG:Debug>,$<PLATFORM_ID:Windows>>:_WIN32_DEBUG>
)

这个表达式只在 Windows + Debug 下生效。我在跨平台项目中经常用这种组合条件,避免在 Linux 上误定义 Windows 相关的宏。

12.2 平台特定源文件选择:$<PLATFORM_ID> 实战

不同平台需要不同的源文件,这是家常便饭。比如 Windows 上用 WinSock,Linux 上用 POSIX socket。

target_sources(myapp PRIVATE
    common.cpp
    $<$<PLATFORM_ID:Windows>:win_socket.cpp>
    $<$<PLATFORM_ID:Linux>:linux_socket.cpp>
)

生成器表达式会在构建时自动选择正确的源文件。你不需要写 if(WIN32) 之类的条件判断,CMake 会帮你搞定。

注意: 生成器表达式只在构建系统生成阶段生效。如果你用 source_group 或 IDE 项目文件,这些源文件在 IDE 里可能仍然显示为灰色不可用。我曾经因为这个排查了半天,后来发现是 IDE 的显示问题,不影响实际编译。

对于更复杂的平台判断,可以用 $<PLATFORM_ID> 配合 STREQUAL

target_sources(myapp PRIVATE
    common.cpp
    $<$<PLATFORM_ID:Windows>:win_socket.cpp>
    $<$<PLATFORM_ID:Linux>:linux_socket.cpp>
    $<$<PLATFORM_ID:Darwin>:mac_socket.cpp>
)

嗯,这里要注意:PLATFORM_ID 的值是 CMake 内部定义的,Windows 对应 Windows,macOS 对应 Darwin,Linux 就是 Linux。别写错了。

12.3 调试与发布不同链接库

这是我在实际项目中遇到最多的场景。Debug 版本需要链接调试库(比如带 d 后缀的库),Release 版本链接优化库。

target_link_libraries(myapp PRIVATE
    $<$<CONFIG:Debug>:mylib_d>
    $<$<CONFIG:Release>:mylib>
)

但更常见的做法是使用 debugoptimized 关键字:

target_link_libraries(myapp PRIVATE
    debug mylib_d
    optimized mylib
)

这两种写法效果一样。我个人更推荐生成器表达式的写法,因为它更灵活——你可以组合更多条件,比如同时判断平台和配置:

target_link_libraries(myapp PRIVATE
    $<$<AND:$<CONFIG:Debug>,$<PLATFORM_ID:Windows>>:mylib_wind>
    $<$<AND:$<CONFIG:Release>,$<PLATFORM_ID:Windows>>:mylib_win>
    $<$<AND:$<CONFIG:Debug>,$<PLATFORM_ID:Linux>>:mylib_linuxd>
    $<$<AND:$<CONFIG:Release>,$<PLATFORM_ID:Linux>>:mylib_linux>
)
避坑指南: 我曾经在项目里用 debug 关键字,结果发现 CMake 对 optimized 的处理有点「宽松」——它会把所有非 Debug 配置都当成 optimized。如果你有 RelWithDebInfo 或 MinSizeRel 配置,它们也会链接 optimized 库。用生成器表达式可以精确控制每个配置的行为。

12.4 安装路径动态计算

安装路径的灵活性,直接决定了你的库能不能被其他项目方便地使用。生成器表达式在安装路径计算中非常有用。

install(TARGETS mylib
    EXPORT MyLibTargets
    LIBRARY DESTINATION $<$<CONFIG:Debug>:lib/debug>$<$<CONFIG:Release>:lib/release>
    ARCHIVE DESTINATION lib/static
    RUNTIME DESTINATION bin
)

这段代码让 Debug 库安装到 lib/debug,Release 库安装到 lib/release。这样用户可以在同一个安装目录下同时保留两种版本的库。

更常见的做法是结合 CMAKE_INSTALL_PREFIX 和生成器表达式:

install(TARGETS mylib
    EXPORT MyLibTargets
    LIBRARY DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/$<CONFIG>
)

这样库会安装到 lib/Debuglib/Release 下。我在一个大型项目中用过这种方案,配合 find_package 的配置模式,用户只需要指定 CMAKE_BUILD_TYPE 就能自动找到对应的库。

我的建议: 如果你在写一个库,最好在安装时也导出目标(EXPORT),这样其他项目用 find_package 导入时,生成器表达式仍然有效。否则,导入的项目可能无法正确区分 Debug 和 Release 库。

12.5 知识体系总览

下面这张图总结了生成器表达式在四个核心场景中的应用逻辑:

生成器表达式 条件编译 $<CONFIG> 控制宏定义 平台源文件选择 $<PLATFORM_ID> 不同链接库 Debug/Release 库 安装路径动态计算 DESTINATION 动态化 核心价值 构建时动态决策,无需多次运行 CMake 支持多配置生成器(Visual Studio、Xcode) 与 install(EXPORT) 配合,实现可移植的包管理

12.6 综合示例:一个完整的实战场景

最后,我把上面所有知识点整合到一个完整的 CMakeLists.txt 中。这个例子来自我去年做的一个网络库项目:

cmake_minimum_required(VERSION 3.20)
project(NetLib VERSION 1.0.0 LANGUAGES CXX)

add_library(netlib SHARED
    src/core.cpp
    src/buffer.cpp
    $<$<PLATFORM_ID:Windows>:src/win_iocp.cpp>
    $<$<PLATFORM_ID:Linux>:src/linux_epoll.cpp>
    $<$<PLATFORM_ID:Darwin>:src/mac_kqueue.cpp>
)

target_include_directories(netlib PUBLIC include)

target_compile_definitions(netlib PRIVATE
    $<$<CONFIG:Debug>:_DEBUG>
    $<$<CONFIG:Release>:NDEBUG>
    NETLIB_VERSION=\"${PROJECT_VERSION}\"
)

target_link_libraries(netlib PRIVATE
    $<$<AND:$<CONFIG:Debug>,$<PLATFORM_ID:Windows>>:ws2_32d>
    $<$<AND:$<CONFIG:Release>,$<PLATFORM_ID:Windows>>:ws2_32>
    $<$<PLATFORM_ID:Linux>:pthread>
)

install(TARGETS netlib
    EXPORT NetLibTargets
    LIBRARY DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/$<CONFIG>
    ARCHIVE DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/static
    RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/bin
)

install(EXPORT NetLibTargets
    FILE NetLibTargets.cmake
    DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/cmake/NetLib
)

这个例子涵盖了条件编译、平台源文件选择、不同链接库、安装路径动态计算四个核心场景。你想想看,如果没有生成器表达式,你得写多少个 if 分支?而且每次新增一个配置,都要改 CMakeLists.txt。生成器表达式让这一切变得优雅多了。

总结一下: 生成器表达式的核心思想是「延迟求值」。它把决策权交给了构建系统生成阶段,而不是 CMake 配置阶段。这让你的 CMakeLists.txt 更简洁、更灵活、更易于维护。我个人觉得,掌握生成器表达式是区分 CMake 新手和老手的重要标志之一。

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