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.omath.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

posted @ 2025-02-21 21:14  PHarr  阅读(4)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示