模板分离编译
💦 什么是分离编译
一个程序 (项目) 由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
💦 模板的分离编译
假如有以下场景,模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:
背景 ❗
在 C语言实现数据结构时,我们写的顺序表、链表等,都是写一个 SeqList.h 文件用于声明,SeqList.c 用于定义,test.c 用于测试。而到 STL 这里都是定义 vector.h 用于声明定义或定义,test.cpp 用于测试。这是因为 C++ 里的模板不支持分离编译。
可以看到这里调用 F 后报了链接错误,链接错误一般都是在链接时找不到它的定义,但是我这里有定义 F 的呀,相比 Print 都找的到,而 F 为啥找不到 ❓
我们先回顾下程序的编译过程 Func.h | Func.cpp | Test.cpp:
-
预处理 —— 头文件展开、宏替换、条件编译、去掉注释后,生成一份干净的 C 原始程序。
Func.i | Test.i
-
编译 —— 语法检查后,生成汇编代码。
Func.s | Test.s
-
汇编 —— 把汇编代码转成二进制机器码
Func.o | Test.o
-
链接 —— 把类似 Test.o 里面 F and Print 这样没有地址的地方,拿修饰过的函数名去 Func.o (符号表里会把函数名和对应地址建立起来) 中查找,找到后填上地址。再把目标文件合并到一起,生成可执行程序。
为什么不能分离编译 ❓
用函数名去查找时 Print 能找到,但是 F 找不到,如下标识处就是 Windows 下函数名 F 的修饰规则修饰出来的函数名。
因为在链接之前,这 2 个文件都是各自玩各自的,只有在链接时才会交汇。Func.i 编译成 Func.s 时就存在一个问题,Print 有定义可以生成,但是 F 是 1 个模板,它不能生成,因为不知道 T 是什么类型,这里只有 Test.i 才知道 T 是什么类型,等到链接时就晚了,所以它找不到 F 的定义。
💦 解决方法
☣ A):
先说一下不可行的方法,让编译器在编译的时候去各个地方查找实例化,比如说在 Func.i 里看到有 1 个模板,然后去 Test.i 里找实例化,但是这样对于编译器的实现就复杂了,这样说的原因是如果是大项目,有几百个文件的情况下,那么成本就非常高了。所以说在链接之前,它们是不会互相交互的。
☣ B):
显示指定实例化,编译器看到后会就知道你要把这个 T 实例化什么类型。
但是显示实例化带来的问题是我换个类型就又链接不上了,因为你这里只显示实例化了 int,解决方法是再显示实例化对应类型,这种方式的缺陷也很明显 —— 使用一种类型就得显示实例化一个,很麻烦,一点也不通用。
☣ C) 推荐:
这种方法非常的粗暴,STL 源码中也是使用这种粗暴的方案,不分离编译,声明和定义或者直接定义在 Func.h 中。因为 Func.h 中包含了模板的定义,就不需要链接的时候去查找了,直接在编译阶段就直接填地址了。有些地方可能会把就种声明和定义放一起的模板,它会定义成 Func.hpp,也就是说它既是 .h,也是 .cpp。
分离编译扩展阅读
💦 补充
同样我们的类模板也不支持分离编译,最好的办法就是不分离,要调用构造、析构,需要找它们的地址,此时就不需要在链接时去找了,在编译时既有声明也有定义,然后这里编译成指令的同时就可以拿到它们的地址了。
按需实例化 ❓
紧接着,我们又实现了一个 push,并且 push 的定义里有一个语法错误 —— 少一个分号,但是奇怪的是我竟然能编译通过。原因其实也很简单,模板如果没有实例化,编译器不会去检查模板函数内部的语法错误。
二、模板总结
【优点】
- 模板复用了代码,节省资源,更快的迭代开发,C++ 的标准模板库 (STL) 也因此而产生。
- 增强了代码的灵活性
【缺点】
-
模板会导致代码膨胀问题,也会导致编译时间变长。
-
出现模板编译错误时,错误信息非常凌乱,且准确度不高 (不要轻易去相信模板的报错),不易定位错误。可能只是一个小错误,却报出一大串的错误 (深有体会),此时一定要优先看第一个错误。
但是整体而言,模板肯定是优点远大于缺点的。
- 点赞
- 收藏
- 关注作者
评论(0)