7、编译选项:target_compile_options 与 target_compile_features
说实话,刚接触 CMake 那会儿,我总觉得设置 C++ 标准就是写个 set(CMAKE_CXX_STANDARD 17) 就完事了。直到有一次,我在一个大型项目里这么干,结果某个第三方库死活编译不过。排查了半天才发现——全局设置把人家库的 C++11 需求给覆盖了。
嗯,从那以后我就学乖了。现代 CMake 的核心思想是「目标导向」,说白了就是每个库、每个可执行文件,都应该自己管好自己的编译需求。今天我们就聊聊两个最常用的命令:target_compile_options 和 target_compile_features。
7.1 为什么不能全局设置?
你想想看,一个项目里可能有几十个目标。有的目标需要 C++17,有的只需要 C++11。如果你在根目录写一句 set(CMAKE_CXX_STANDARD 17),那所有目标都被迫升级到 C++17。这合理吗?
我个人习惯是:每个目标只声明自己需要什么,而不是让父目录替它做决定。这样项目结构清晰,迁移也方便。
7.2 target_compile_features:声明你需要的 C++ 特性
这个命令是我最常用的。它不直接说「我要 C++17」,而是说「我要某个编译器特性」。CMake 会自动帮你选一个支持该特性的最低标准。
# 声明需要 C++11 的 auto 类型推导
target_compile_features(my_lib PUBLIC cxx_auto_type)
# 声明需要 C++14 的泛型 lambda
target_compile_features(my_lib PUBLIC cxx_generic_lambdas)
# 声明需要 C++17 的 if constexpr
target_compile_features(my_lib PUBLIC cxx_if_constexpr)
这里有个细节:PUBLIC 表示这个特性会传播给依赖它的目标。如果你写 PRIVATE,那就只有当前目标自己知道。
PUBLIC 声明特性。因为如果你的头文件里用了 if constexpr,那所有包含这个头文件的目标,编译器都得支持 C++17。用 PUBLIC 能自动传递这个约束。
7.3 target_compile_options:精细控制编译器标志
有时候光声明特性不够。比如你想开启所有警告、关闭某个特定警告、或者加一个优化标志。这时候就得用 target_compile_options。
# 为 my_lib 开启 -Wall -Wextra
target_compile_options(my_lib PRIVATE -Wall -Wextra)
# 如果是 MSVC,需要不同的写法
if(MSVC)
target_compile_options(my_lib PRIVATE /W4)
else()
target_compile_options(my_lib PRIVATE -Wall -Wextra -Wpedantic)
endif()
注意我用了 PRIVATE。为什么?因为编译选项通常只影响当前目标的编译过程,不应该泄漏给依赖方。你想想看,如果 A 库加了 -Werror,结果 B 库链接了 A,B 的编译也被强制 -Werror,这多冤啊。
-Werror 写成了 PUBLIC,结果下游所有目标都跟着报错。排查了整整一下午才发现是传播问题。所以记住:编译选项默认用 PRIVATE,除非你明确知道为什么要用 PUBLIC。
7.4 如何正确设置 C++ 标准?
现在你应该明白了:不要用全局变量。正确的做法是结合 target_compile_features 和 target_compile_options。
# 推荐做法:用特性声明
target_compile_features(my_app PUBLIC cxx_std_17)
# 或者更精确地声明具体特性
target_compile_features(my_app PUBLIC
cxx_if_constexpr
cxx_structured_bindings
cxx_fold_expressions
)
cxx_std_17 是 CMake 3.8 引入的便捷写法,表示「我需要 C++17 标准」。CMake 会自动设置 -std=c++17 或 /std:c++17。
| CMake 特性名 | 对应 C++ 标准 | 常见编译器标志 |
|---|---|---|
| cxx_std_11 | C++11 | -std=c++11 |
| cxx_std_14 | C++14 | -std=c++14 |
| cxx_std_17 | C++17 | -std=c++17 |
| cxx_std_20 | C++20 | -std=c++20 |
| cxx_std_23 | C++23 | -std=c++23 |
7.5 实战:一个完整的例子
我们来看一个实际项目中的 CMakeLists.txt。假设我们有一个核心库和一个可执行文件:
# 核心库:只需要 C++11,但需要开启所有警告
add_library(core core.cpp core.h)
target_compile_features(core PUBLIC cxx_std_11)
target_compile_options(core PRIVATE
$<$<CXX_COMPILER_ID:MSVC>:/W4>
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-Wall -Wextra>
)
# 可执行文件:需要 C++17,并且链接 core
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE core)
target_compile_features(my_app PRIVATE cxx_std_17)
target_compile_options(my_app PRIVATE -O2)
这里用了生成器表达式 $<...>,根据编译器类型自动选择不同的标志。这个技巧我在跨平台项目里经常用,省去了写一堆 if-else 的麻烦。
cxx_std_11,但 my_app 声明了 cxx_std_17。两者互不干扰。my_app 编译时用 C++17,core 编译时用 C++11。这就是目标导向的魅力。
7.6 避坑指南
最后分享几个我踩过的坑:
- 不要混用全局和局部设置。如果你既写了
set(CMAKE_CXX_STANDARD 17)又写了target_compile_features,CMake 的行为可能会让你困惑。建议只用一种方式。 - 注意特性的可用性。不是所有编译器都支持所有特性。比如
cxx_if_constexpr在 GCC 5 以下就不支持。建议在 CMakeLists.txt 里用CMAKE_CXX_COMPILE_FEATURES变量检查一下。 - PUBLIC vs PRIVATE 要分清。特性用 PUBLIC 传播,编译选项用 PRIVATE 隔离。这个原则能避免很多莫名其妙的编译错误。
好了,关于编译选项和 C++ 标准设置,今天就聊这么多。记住一句话:每个目标只说自己需要什么,别替别人做决定。这样你的项目才能经得起时间考验。