模板函数的分离编译

程序编译原理详情
在介绍分离编译之前,应当先做到对基本的程序编译过程有一定的了解。
下面图文并茂的理解一下这个过程。

基础程序编译过程

在这个编译器工作过程中,最主要的是明白:最终的所有的目标文件.o链接起来生成单一的可执行程序的过程.
模板不支持分离编译, 我所理解的是因为函数的定义和声明没有一起放在.h文件中,在链接时,找不到函数的定义,导致程序无法执行。为什么中不到函数的定义,这就又是更深一层的东西,让给我们来抽丝剥茧的理解这个问题。
下面看这个例子:

test.h
#pragma once;
#include<iostream>
template<class T>
void fun();


test.cpp
#include<iostream>
using namespace std;
template<class T>
void fun()
{
    cout << "fun()" << endl;
}


main.cpp
#include<iostream>
#include"test.h"
using namespace std;

int main()
{
    fun();
    return 0;
}

这个例子中编译器在main()函数使用f()处并不知道函数fun的定义,因为它不在test.h里面,于是编译器只好寄希望于连接器,希望它能够在其他.obj里面找到函数fun的实例,在本例中就是test.obj,然而,后者中真有函数fun的二进制代码吗?NO!!!
因为C++标准明确表示,当一个模板不被用到的时侯它就不该被实例化出来,test.cpp中用到了函数fun了吗?
没有!!所以实际上test.cpp编译出来的test.obj文件中关于A::fun一行二进制代码也没有,于是连接器就傻眼了,只好给出一个连接错误。但是,如果在test.cpp中写一个函数,其中调用函数fun,则编译器会将其实例化出来,因为在这个点上(test.cpp中),编译器知道模板的定义,所以能够实例化,于是,test.obj的符号导出表中就有了函数fun这个符号的地址,于是连接器就能够完成任务。

关键是:
在分离式编译的环境下,编译器编译某一个.cpp文件时并不知道另一个.cpp文件的存在,也不会去查找(当遇到未决符号时它会寄希望于连接器)。这种模式在没有模板的情况下运行良好,但遇到模板时就傻眼了,因为模板仅在需要的时候才会实例化出来,所以,当编译器只看到模板的声明时,它不能实例化该模板,只能创建一个具有外部连接的符号并期待连接器能够将符号的地址决议出来。然而当实现该模板的.cpp文件中没有用到模板的实例时,编译器懒得去实例化,所以,整个工程的.obj中就找不到一行模板实例的二进制代码,因此就会出现链接错误。

解决这类问题最好的办法就是:
把声明和定义放到一个文件xxx.hpp中。
还有比较不推荐的,在模版头文件xxx.h文件里面显示实例化,模版类的定义后面添加 template void fun()一是因为一些编译器不支持,而是因为每次使用一次模版,都需要把实例化声明在下边。

### 3.1 模板类中模板函数分离编译实现 在 C++ 中,模板类中的模板函数无法像普通函数那样进行分离编译,其根本原因在于模板的实例化发生在编译阶段,编译器必须在编译时看到模板的完整定义才能生成具体的代码。如果将模板函数的定义放在 `.cpp` 文件中,而只在头文件中声明,则在其他翻译单元中使用该模板函数时,编译器无法找到其实现,从而导致链接错误[^2]。 ### 3.2 使用 `.tpp` 文件实现分离编译 一种常见的做法是将模板类的实现代码放在一个 `.tpp` 文件中,并在头文件中通过 `#include` 引入该文件。这种方式虽然没有真正实现物理上的分离编译,但逻辑上将接口和实现分离,提高了代码的可维护性。 ```cpp // DequeProcessor.h #pragma once #include <deque> #include <iostream> template <typename T> class DequeProcessor { public: void print(const std::deque<T>& dq); }; #include "DequeProcessor.tpp" ``` ```cpp // DequeProcessor.tpp template <typename T> void DequeProcessor<T>::print(const std::deque<T>& dq) { for (const auto& val : dq) { std::cout << val << " "; } std::cout << std::endl; } ``` ```cpp // main.cpp #include "DequeProcessor.h" int main() { std::deque<int> dq = {1, 2, 3, 4, 5}; DequeProcessor<int> processor; processor.print(dq); return 0; } ``` 该方法确保模板定义在编译时可见,避免了链接错误,适用于模板实现较为复杂、希望保持头文件简洁的场景。 ### 3.3 使用显式模板实例化 另一种方式是将模板函数的定义放在 `.cpp` 文件中,并在该文件中对特定类型进行显式实例化。这种方法适用于模板参数类型已知且有限的场景。 ```cpp // DequeProcessor.cpp #include "DequeProcessor.h" template <typename T> void DequeProcessor<T>::print(const std::deque<T>& dq) { for (const auto& val : dq) { std::cout << val << " "; } std::cout << std::endl; } // 显式实例化 template class DequeProcessor<int>; template class DequeProcessor<double>; ``` ```cpp // DequeProcessor.h #pragma once #include <deque> #include <iostream> template <typename T> class DequeProcessor { public: void print(const std::deque<T>& dq); }; ``` 此方法虽然实现了 `.cpp` 文件中的实现逻辑,但需要为每一个使用类型显式实例化,若尝试使用未被显式实例化的类型,会导致链接错误。因此,这种方法在灵活性上存在限制。 ### 3.4 适用场景与限制 - **适用场景**: - 使用 `.tpp` 文件组织模板实现,适用于希望保持接口与实现逻辑分离的项目。 - 使用显式实例化适用于模板参数类型已知且有限的项目。 - **限制**: - 模板定义必须在编译时对所有使用模板的翻译单元可见,否则会导致链接错误。 - 显式实例化无法支持所有可能的类型组合,维护成本较高。 ---
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值