precompilation-headers 以及在cmake中的实现
目录
- 1.gcc/g++ 预编译头文件
- 1.1.头文件
- 1.2.main.cpp
- 1.3.目录结构
- 1.4.编译
- 1.5.测试
- 2.cmake 中使用预编译头文件
- 3.cmake中预编译头文件不能使用install()命令导出
- 3.1.核心概念解读
- 3.2.为什么需要谨慎对待?
- 3.3.正确的实践方法
- 3.4.总结
- 4.头文件的处理
- 4.1.两种包含语法的含义
- 4.2.示例
- 参考资料
1.gcc/g++ 预编译头文件
1.1.头文件
为了测试,把常用c++标准头文件都放在一起.
// my.h
#ifndef MY_H
#define MY_H// 添加你最常用的稳定头文件
#include <algorithm>
#include <iomanip>
#include <list>
#include <ostream>
#include <streambuf>
#include <bitset>
#include <ios>
#include <locale>
#include <queue>
#include <string>
#include <complex>
#include <iosfwd>
#include <map>
#include <set>
#include <typeinfo>
#include <deque>
#include <iostream>
#include <memory>
#include <sstream>
#include <utility>
#include <exception>
#include <istream>
#include <new>
#include <stack>
#include <valarray>
#include <fstream>
#include <iterator>
#include <numeric>
#include <stdexcept>
#include <vector>
#include <array>
#include <condition_variable>
#include <mutex>
#include <scoped_allocator>
#include <type_traits>
#include <atomic>
#include <forward_list>
#include <random>
#include <system_error>
#include <typeindex>
#include <chrono>
#include <future>
#include <ratio>
#include <thread>
#include <unordered_map>
#include <codecvt>
#include <initializer_list>
#include <regex>
#include <tuple>
#include <unordered_set>
#include <shared_mutex>
#include <any>
#include <execution>
#include <memory_resource>
#include <string_view>
#include <variant>
#include <charconv>
#include <filesystem>
#include <optional>
#include <cassert>
#include <clocale>
#include <cstdarg>
#include <cstring>
#include <cctype>
#include <cmath>
#include <cstddef>
#include <ctime>
#include <cerrno>
#include <csetjmp>
#include <cstdio>
#include <cwchar>
#include <cfloat>
#include <csignal>
#include <cstdlib>
#include <cwctype>
#include <climits>
#include <cfenv>
#include <cinttypes>
#include <cstdint>
#include <cuchar>// 如果需要,也可以包含一些稳定的自定义头文件
// #include "my_stable_lib.h"#endif // MY_H
1.2.main.cpp
#include "my.h"int main(void)
{std::cout<<"Hello World!"<<std::endl;return 0;
}
1.3.目录结构
├── include
│ └── my.h
└── main.cpp
1.4.编译
# 编译 预编译头文件 my.h.gch
cd include/
g++ -x c++-header my.h -o my.h.gch
1.5.测试
两种方式使用预编译头文件:
- 自动使用预编译头文件
time g++ -I./include main.cpp -o main
# 可以看到运行时间是0.47s
real 0m0.470s
user 0m0.226s
sys 0m0.037s
- 显式使用预编译头文件
time g++ -I./include -include my.h main.cpp -o main
# 可以看到也是起作用的
real 0m0.457s
user 0m0.240s
sys 0m0.042s
注意:显式指定预编译头文件,在代码中可以不包含头文件,例如main.cpp代码如下:
int main(void)
{std::cout<<"Hello World!"<<std::endl;return 0;
}
对比不使用预编译头文件:
time g++ -I./include -include my.h main.cpp -o main
# 需要1.795s
real 0m1.795s
user 0m1.031s
sys 0m0.176s
2.cmake 中使用预编译头文件
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)set(CMAKE_EXPORT_COMPILE_COMMANDS ON)project(precompiled_headers_test LANGUAGES CXX)
add_executable(main main.cpp)
# 使用预编译头文件
target_precompile_headers(main PRIVATE ${CMAKE_SOURCE_DIR}/include/my.h)
target_include_directories(main PRIVATE ${CMAKE_SOURCE_DIR}/include)
include/my.h被用于预编译头文件.
cmake -S . -B build
time cmake --build build
# 修改main.cpp对比编译时间
3.cmake中预编译头文件不能使用install()命令导出
在cmake中使用precompilation headers有如下疑问:
however, this shouldn’t be done for targets exported with the
install()command.
Other porjects shouldn’t be forced to comsume our precompiled headers(as it’s unconventional).
为什么不能用于install()命令?这段英文的具体含义是什么?
这个问题,确实是使用 CMake 预编译头文件时一个非常重要且精妙的设计考量.
简单来说,其核心思想是:保持接口的干净,将实现细节隐藏起来,避免给使用你项目的人带来意外的麻烦.
下面我为你详细解释一下这段英文的含义和背后的原因.
3.1.核心概念解读
首先,我们来理解两个关键点:
install()命令与项目发布: CMake 的install()命令用于定义如何将你的库(包括头文件,库文件等)安装到系统中,
以便其他项目能够通过find_package()来找到并使用它.这相当于你制作了一个“产品”发布给他人.- 预编译头文件(PCH)的性质: PCH 是一个实现细节和构建优化手段.它和编译器,编译选项等强相关,目的是为了加快你自身项目的编译速度.
3.2.为什么需要谨慎对待?
将这两点结合起来,问题就清晰了:
- PCH 不是接口契约:你的库对外提供的接口(API)是其头文件中的函数,类声明等.
而 PCH 是为了生成这些接口的最终二进制文件过程中的一个加速工具.强制下游项目(即使用你的库的项目)也使用你的 PCH,
就像强迫别人必须用和你一模一样的工具链来组装他们自己的产品一样,是不合理且具有侵入性的. - 兼容性问题:不同项目的编译环境(编译器版本,编译标志,预定义宏等)可能截然不同.
你的 PCH 是在你的特定构建环境下生成的,直接强加给另一个项目,极有可能因为环境不兼容而导致编译失败, 或产生难以排查的警告(例如 GCC 会报 -Winvalid-pch 警告). - 控制权反转:构建时应使用哪些 PCH,这属于构建策略,理应由每个项目自己决定.
如果你的库通过导出目标将 PCH 设置为 PUBLIC 或 INTERFACE 属性,就相当于剥夺了下游项目的这个控制权.
3.3.正确的实践方法
那么,应该如何正确地使用 target_precompile_headers 呢?CMake 官方文档和建议如下:
- 优先使用
PRIVATE作用域:
对于大多数不打算被其他项目使用的可执行文件或静态库,应使用 PRIVATE 关键字.这确保 PCH 只用于当前目标本身.
# 正确:PCH 仅用于 MyApp 自身的构建,不会传播出去
target_precompile_headers(MyApp PRIVATE <vector> <string> "my_stable_headers.h")
- 导出目标时的保护措施:
如果你的目标会被install(EXPORT)导出供他人使用,但又希望在其内部使用 PCH 来加速编译,那么需要采取隔离措施.
使用生成器表达式进行隔离:
如果确有需要(例如,你创建一个专门用于提供统一 PCH 的接口库),一个更稳妥的做法是使用$<BUILD_INTERFACE:...>生成器表达式.
这可以确保 PCH 设置只在当前构建树(build tree)内有效,而不会包含在安装导出的目标文件里.
# 创建一个专门管理 PCH 的接口库(谨慎使用)
add_library(my_project_pch INTERFACE)
# 使用 BUILD_INTERFACE 确保只在构建时生效,安装时被排除
target_precompile_headers(my_project_pch INTERFACE$<BUILD_INTERFACE:<string>>$<BUILD_INTERFACE:<vector>>
)
# 其他目标在构建时链接这个库以获取 PCH
target_link_libraries(MyApp PRIVATE my_project_pch)
3.4.总结
总而言之,这段英文提示的本质是软件工程中的一条最佳实践:明确定义接口,并最小化接口的约束.
预编译头文件是一个强大的构建期优化工具,但它应该被当作内部实现细节来处理.
当发布你的项目以供他人使用时,应当提供一个干净,无附加条件的接口,让使用者拥有对其自身构建过程的完全控制权.
4.头文件的处理
有如下内容让人费解:
- header.h is interpreted as relative to the current source directly and will be included with an absolute path.
[["header.h"]]is interpreted according to the compiler’s implementation and is usually found in the
INCLUDE_DIRECTORIES variable. Use target_include_directories() to configure it.
你在CMake书籍中读到的这段内容,精确地指出了头文件包含方式与CMake路径配置的核心关系.这确实是理解现代CMake依赖管理的关键一环.
4.1.两种包含语法的含义
书中的描述区分了两种头文件包含方式在CMake视角下的不同含义:
- header.h(无引号或尖括号):这种写法在CMake中被明确地解释为相对于当前源文件目录(即
CMAKE_CURRENT_SOURCE_DIR)的路径.
CMake在处理时,会倾向于将其转换为一个绝对路径.这意味着,无论你的包含路径(INCLUDE_DIRECTORIES)如何设置,编译器最终接收到的很可能是一个完整的绝对路径. "header.h"或<header.h>: 这种带引号或尖括号的写法,是C/C++标准中的正式语法.CMake不会像上一种情况那样对其进行“特殊处理”或强制转换.
编译器会根据其自身的搜索规则来查找这个文件,而搜索的范围,正依赖于通过target_include_directories()命令设置的INCLUDE_DIRECTORIES变量.
4.2.示例
MyProject/
├── CMakeLists.txt
├── include/
│ └── mylib.h
├── src/
│ ├── mylib.cpp
│ └── main.cpp
cmake_minimum_required(VERSION 3.10)
project(MyProject)# 创建库目标
add_library(mylib src/mylib.cpp)
# 公共头文件目录对mylib自身和链接它的目标均可见
target_include_directories(mylib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)# 创建可执行文件目标
add_executable(myexe src/main.cpp)
# 可执行文件链接库,会自动获得mylib的PUBLIC头文件路径
target_link_libraries(myexe PRIVATE mylib)
这样,当编译 myexe时,编译器就能在 ${CMAKE_CURRENT_SOURCE_DIR}/include目录下找到 #include "mylib.h"这个头文件了.
参考资料
- 3.22 使用预编译头
- Using Precompiled Headers
