C++ 为什么模板要写在仅标头库中?
什么是仅标头库?
以下摘自维基百科
在一个C或者C++语言的程序代码中,一个在头文件便已包含了所有宏、函数和类的实现,而且在包含了头文件后这些实现都可以被编译器读取访问;以这种头文件所实现的函数库便叫做仅标头库或纯头文件函数库(Header-only)。仅标头库并不需要分开编译、数据包和安装即可使用;只需指导编译器到该些头文件的路径,还有使用#include预处理器导入该些头文件进应用程序代码即可使用。此外,因程序代码的可读性和存在,编译器的优化器可以更佳地扫描代码。
缺点如下:
- 脆弱性——对该库的大多数变更都需要重新编译使用该库的所有编译翻译单元
- 编译时间变长——编译器必须编译导入文件中所有的组件实现,而不仅仅是它们的接口
- 代码膨胀(有争议)——在非类别函数中必要使用内联语句可能会因过度使用而导致代码膨胀。
尽管如此,仅标头库仍很受欢迎,因为它避免了(通常比上述更严重的)打包问题。
简单来说就是把原本的.h/.hpp
和.c/.cpp
合并到一个文件。
为什么模板要写在仅标头库中?
想看一个简单的例子。
main.cpp
#include "math.h"
int main(){
int x = ADD<int>(1 , 2);
return 0;
}
math.h
template<typename T>
T ADD(T a, T b);
math.cpp
template<typename T>
T ADD(T a, T b){
return a + b;
}
然后我们参考C++ 编译过程 简要分析,先执行如下命令
g++ -E math.cpp -o math.i
g++ -E main.cpp -o main.i
得到main.i
# 0 "main.cpp"
# 0 "<built-in>"
# 0 "<命令行>"
# 1 "main.cpp"
# 1 "math.h" 1
template<typename T>
T ADD(T a, T b);
# 2 "main.cpp" 2
int main(){
int x = ADD<int>(1 , 2);
return 0;
}
以及math.i
# 0 "math.cpp"
# 0 "<built-in>"
# 0 "<命令行>"
# 1 "math.cpp"
template<typename T>
T ADD(T a, T b){
return a + b;
}
先看math.i
文件,其实是包含了一个完整的函数定义和实现。
但我们再看main.i
,只有函数的定义,并没有实现,此时我们进行继续执行
g++ -S main.i -o main.s
g++ -S math.i -o math.s
g++ -c main.i -o main.o
g++ -c math.i -o math.o
g++ main.o math.o -o math.exe
就会得到如下报错
/usr/lib/gcc/x86_64-pc-cygwin/11/../../../../x86_64-pc-cygwin/bin/ld: main.o:main.cpp:(.text+0x18): undefined reference to `int ADD<int>(int, int)'
collect2: 错误:ld 返回 1
因为预处理、编译、汇编都是独立进行的,链接的时候 main.o
和math.h
依旧没有包含int ADD<int>(int,int)
函数的实现
如何解决?
一种思路是直接强制进行模板特化。
把math.cpp
修改如下
template<typename T>
T ADD(T a, T b){
return a + b;
}
template int ADD(int a, int b);
这样做的话,我们再对math.cpp
编译,就会对ADD
函数进行特化。(math.i
如下)
# 0 "math.cpp"
# 0 "<built-in>"
# 0 "<命令行>"
# 1 "math.cpp"
template<typename T>
T ADD(T a, T b){
return a + b;
}
template int ADD(int a, int b);
这种方法的缺点就是我们只能使用已经特化的函数,这样就失去了泛型的意义。
还有另一种思路是我们在主函数中#include <math.cpp>
,预处理后的main.i
如下
# 0 "main.cpp"
# 0 "<built-in>"
# 0 "<命令行>"
# 1 "main.cpp"
# 1 "math.cpp" 1
template<typename T>
T ADD(T a, T b){
return a + b;
}
# 2 "main.cpp" 2
int main(){
int x = ADD<int>(1 , 2);
return 0;
}
可以看到这样的话就包含了完整的函数实现。
对于第二种方法其实不符合我们的一般规则。因此我们就可以使用 仅标头库。修改后代码如下
main.cpp
#include "math.hpp"
int main(){
int x = ADD<int>(1 , 2);
return 0;
}
math.hpp
template<typename T>
T ADD(T a, T b);
template<typename T>
T ADD(T a, T b){
return a + b;
}
这里使用.hpp
只是为了强制要求采用c++
的标准。到这里也可以想到,.c/.cpp/.cc/.h/.hpp
等所谓的后缀只是给程序员看,对于编译器来说都是纯文本罢了。
补充一点std::vector
等各种STL实现也是用Header-Only
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了