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>
)
但更常见的做法是使用 debug 和 optimized 关键字:
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 和 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/Debug 或 lib/Release 下。我在一个大型项目中用过这种方案,配合 find_package 的配置模式,用户只需要指定 CMAKE_BUILD_TYPE 就能自动找到对应的库。
find_package 导入时,生成器表达式仍然有效。否则,导入的项目可能无法正确区分 Debug 和 Release 库。
12.5 知识体系总览
下面这张图总结了生成器表达式在四个核心场景中的应用逻辑:
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。生成器表达式让这一切变得优雅多了。
公众号:蓝海资料掘金营,微信deep3321