编译选项:target_compile_options 和 add_compile_options 设置编译标志
编译选项这东西,说白了就是告诉编译器「你该怎么干活」。是开启优化?还是加调试信息?要不要支持 C++17?这些都得靠编译标志来传达。我在项目中见过太多人直接在源码里写 #pragma,或者一股脑把所有标志塞到 CMAKE_CXX_FLAGS 里——嗯,这两种做法都有点糙。
CMake 给了我们两个更优雅的工具:add_compile_options 和 target_compile_options。名字很像,但用法天差地别。今天我们就把它俩掰扯清楚。
1. 全局 vs 局部:add_compile_options
add_compile_options 是全局的。你调用一次,它会影响当前 CMakeLists.txt 及其所有子目录中的所有目标。说白了就是「一刀切」。
# 给所有目标加上 -Wall -Wextra
add_compile_options(-Wall -Wextra)
add_library(my_lib src/lib.cpp)
add_executable(my_app src/main.cpp)
# 这两个目标都会带上 -Wall -Wextra
我刚开始用 CMake 时,觉得这玩意儿真方便。一行代码,所有目标都照顾到了。但后来踩了个坑——有个第三方库的子目录,它自己的编译选项跟我冲突了,结果整个项目编译不过。那时候我才意识到:全局选项是把双刃剑。
3. 精准控制:target_compile_options
target_compile_options 是局部的。它只影响你指定的那个目标。这才是现代 CMake 推荐的做法——你想想看,每个目标都有自己的需求,干嘛非要绑在一起?
add_library(my_lib src/lib.cpp)
add_executable(my_app src/main.cpp)
# 只给 my_lib 加 -fPIC
target_compile_options(my_lib PRIVATE -fPIC)
# 只给 my_app 加 -O2
target_compile_options(my_app PRIVATE -O2)
这里有个关键点:PRIVATE、INTERFACE、PUBLIC 三个关键字。我简单解释一下:
- PRIVATE:选项只影响当前目标,不影响依赖它的目标
- INTERFACE:选项不影响当前目标,但会传递给依赖它的目标
- PUBLIC:选项既影响当前目标,也传递给依赖它的目标
举个例子你就明白了:
add_library(my_lib STATIC src/lib.cpp)
# 编译 my_lib 时需要 -fPIC,链接 my_lib 的人不需要
target_compile_options(my_lib PRIVATE -fPIC)
add_library(my_header_only INTERFACE)
# 使用这个头文件库的人需要 -std=c++17
target_compile_options(my_header_only INTERFACE -std=c++17)
add_executable(my_app src/main.cpp)
target_link_libraries(my_app PRIVATE my_lib my_header_only)
# my_app 会继承 my_header_only 的 -std=c++17,但不会继承 my_lib 的 -fPIC
3. 生成器表达式:让选项更智能
有时候你需要根据编译器或构建类型来设置不同的选项。这时候生成器表达式就派上用场了。说白了,它就是在 CMake 生成构建系统时才求值的「条件表达式」。
add_executable(my_app src/main.cpp)
# Debug 模式加 -g,Release 模式加 -O2
target_compile_options(my_app PRIVATE
$<$<CONFIG:Debug>:-g>
$<$<CONFIG:Release>:-O2>
)
# 如果是 GCC 或 Clang,加 -Wall
target_compile_options(my_app PRIVATE
$<$<OR:$<CXX_COMPILER_ID:GNU>,$<CXX_COMPILER_ID:Clang>>:-Wall>
)
我第一次看到这种写法时,觉得「这什么鬼?跟天书一样」。但用习惯了就会发现,它真的很强大。尤其是跨平台项目,不同编译器、不同配置,一套 CMakeLists.txt 全搞定。
4. 实战:一个完整的例子
来,我们看一个实际项目中的用法。假设你有一个库和一个可执行文件,库需要编译成位置无关代码,可执行文件需要开启所有警告:
cmake_minimum_required(VERSION 3.15)
project(CompileOptionsDemo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 库目标
add_library(my_math src/math.cpp)
target_compile_options(my_math PRIVATE
$<$<CONFIG:Debug>:-g -O0>
$<$<CONFIG:Release>:-O2 -DNDEBUG>
)
# 如果是 Linux,加 -fPIC
if(UNIX)
target_compile_options(my_math PRIVATE -fPIC)
endif()
# 可执行文件
add_executable(calculator src/main.cpp)
target_link_libraries(calculator PRIVATE my_math)
# 给可执行文件加警告选项
target_compile_options(calculator PRIVATE
$<$<OR:$<CXX_COMPILER_ID:GNU>,$<CXX_COMPILER_ID:Clang>>:
-Wall -Wextra -Wpedantic>
$<$<CXX_COMPILER_ID:MSVC>:/W4>
)
你看,每个目标的编译选项都是独立管理的。库只需要 -fPIC,可执行文件需要警告选项,互不干扰。这就是 target_compile_options 的魅力。
5. 避坑指南
我这些年用下来,总结了几条经验:
- 别混用全局和局部:如果你用了
add_compile_options又用target_compile_options,后者会追加到前者后面。有时候顺序会影响编译结果,容易出问题。 - 生成器表达式里的引号:如果选项包含空格,记得用引号包起来。比如
"-D MY_MACRO=1",否则 CMake 会把它拆成两个参数。 - MSVC 的特殊性:MSVC 的编译选项以
/开头(比如/W4),而 GCC/Clang 用-。生成器表达式可以帮你优雅地处理这个差异。
-std=c++17 写成了 PRIVATE,结果依赖这个库的可执行文件编译时用了默认的 C++14 标准,模板代码报了一堆错。后来改成 INTERFACE 才解决。记住:编译标准这种「传染性」选项,要用 INTERFACE 或 PUBLIC。
6. 知识体系总览
下面这张图帮你理清 add_compile_options 和 target_compile_options 的核心区别:
说白了,add_compile_options 是「粗放式管理」,适合小项目或原型阶段。target_compile_options 是「精细化管理」,适合中大型项目。我个人建议你从一开始就养成用 target_compile_options 的习惯——等你项目膨胀到几十个目标时,你会庆幸当初没偷懒。
公众号:蓝海资料掘金营,微信deep3321