深入浅出之STL源码分析3_类模版实例化与特化
在 C++ 中,类模板的实例化(Instantiation)和特化(Specialization) 是模板编程的核心概念,而 显式实例化(Explicit Instantiation)和隐式实例化(Implicit Instantiation) 是实例化的两种具体方式。
模版的特化和实例化是两个不同等级的概念,不能等同。
特化和实例化是两个层面的事,特化还是提供一种特殊的模子,而最终是否生成代码,还要看是否能够进行实例化,只有实例化之后,才能生成真正的代码,后面我会详细的说下区别。
以下是清晰的分辨和对比:
1. 核心概念
1.1 类模板的实例化(Instantiation)
- 定义:
将模板的泛型类型参数(T
)替换为具体类型(如int
,string
),生成具体的类代码的过程。 - 本质:
编译器根据模板生成实际的类代码(例如vector<int>
是由vector<T>
实例化而来)。
1.2 类模板的特化(Specialization)
- 定义:
针对特定类型参数,提供与通用模板不同的实现。分为 全特化(Full Specialization) 和 偏特化(Partial Specialization)。 - 本质:
为特定类型定制模板行为(例如为bool
类型优化vector
的存储方式)。
2. 显式实例化 vs 隐式实例化
模版的实例化分为显示的实例化和隐式的实例化。
模版的实例化指函数模版(类模版)生成模版函数(模板类)的过程,对于函数模版而言,模版实例化之后,会生成一个真正的函数,而类模版实例化后,对于隐式实例化,只是完成了类的定义,类模版的成员函数(包括构造函数)需要到调用的时候,才会被初始化,而如果显示调用,则不论是否调用,都会直接生成普通函数,和类成员函数。
还有最重要的一点就是,我们最后一定要通过查看汇编,来验证我们的结论,也就是最终的汇编代码中有对应的成员函数,才证明生成了对应的代码,如果没有在汇编里生成,证明就是没有作用,这里可以使用 这个线上工具进行验证,Compiler Explorer
2.1.隐式实例化
2.1.1 模版隐式实例化的定义
其中隐式的实例化是我们平时最常用的实例化方式。隐式实例化,或者说按需实例化on-demand,是当我们要用这个模板生成实体的时候,要创建具体对象的时候,才做的实例化。
2.1.2 类模版的隐式实例化
类模版隐式实例化是指(on-demand),在使用模版类时,才将模版实例化,相对于类模板显示实例化而言的。
看下面具体的代码例子:
#include<iostream>
using namespace std;
template <typename T1,typename T2>
class A{
public:A(){}void Max(T1 t1,T2 t2){}T1 Aa;T2 Bb;
};
int main(int argc,char *argv[]){A<double ,double> temp;//只有这一种调用方式,也就是说只能是显示调用.// A<>temp1; // 这个是会报错的。//temp.Max(3.4,4.5);return 0;
}
我们用Compiler Explorer 进行查看汇编数据,会很清晰,对于on_demand实例化,或者隐式实例化
我们有个概念叫,成员函数惰性生成。也就是调用的时候,才会生成,比方上面的例子中,//temp.Max(3.4,4.5);注释掉后,只是会调用构造函数。
汇编结果如下,符合我们的预期:
而当我们把第二个Max函数的调用放开后,结果如下:
2.2.显示实例化
2.2.1 模版显示实例化的定义
显示实例化,也称为外部实例化,在不使用类模版的时候,将类模版实例化,
显示实例化不是按需的,也就是不论是否调用对应的类成员函数,都会生成对应的成员函数。
2.2.2 类模版的显示实例化
对于类模版而言,不管是否生成一个模版类对象,都可以直接通过显示实例化声明,将类模板实例化为模版类。
格式为:
template class [类模版名]
#include<iostream>
using namespace std;template <typename T1,typename T2>
class A{
public:void Max(T1 t1,T2 t2){}T1 Aa;T2 Bb;
};// template class A<int ,int>;int main(int argc,char *argv[]){return 0;
}
然后用指令把它生成汇编语言
g++ -std=c++11 -S main.cpp -o main.s
我们查看这个文件 main.s 里面是找不到 class A Max 函数。
我们还是直接用工具进行验证,这样的方式比较直观一些:
而如果我们把上面的注释去掉:
#include<iostream>
using namespace std;template <typename T1,typename T2>
class A{
public:void Max(T1 t1,T2 t2){}T1 Aa;T2 Bb;
};template class A<int ,int>;int main(int argc,char *argv[]){return 0;
}
g++ -std=c++11 -S main.cpp -o main.s
我们查看这个文件 main.s 里面是能找到 class A func 函数和类A的Max成员函数。
说明已经实例化成功了。
2.2.3 显示实例化和隐示实例化优缺点对比
当我们在代码中使用了一个模板,触发了一个实例化过程时,编译器就会用模板的实参(Arguments)去替换(Substitute)模板的形参(Parameters),生成对应的代码。同时,编译器会根据一定规则选择一个位置,将生成的代码插入到这个位置中,这个位置被称为 POI(point of instantiation)。由于要做替换才能生成具体的代码,因此 C++ 要求模板的定义对它的 POI 一定要是可见的。换句话说,在同一个翻译单元(Translation Unit)中,编译器一定要能看到模板的定义,才能对其进行替换,完成实例化。因此最常见的做法是,我们会将模板定义在头文件中,然后再源文件中
#include 头文件来获取该模板的定义。这就是模板编程中的包含模型(Inclusion Model)。
所以我们一般情况下,对于模版会把定义放在头文件中。但是这样操作会带来缺点。这个也对应着显示实例化的优点。
现在的一些 C++ 库,整个项目中就只有头文件,没有源文件,库的逻辑全部由模板实现在头文件中。而且这种做法似乎越来越流行,在 GitHub 和 boost 中能看到很多很多。我想原因一个是 C++ 缺乏一个官方的 package manager,这样发布的软件包更易使用(include就行了);另一个就是模板实例化的这种要求
但包含模型也有自身的问题。在一个翻译单元(Translation Unit)中,同一个模板实例只会被实例化一次。也就是对同一个模板传入相同的实参,编译器会先检查是否已实例化过,如果是则使用之前实例化的结果。但在不同的翻译单元中,相同实参的模板会被实例化多次,从而产生多个相同的类型、函数和变量。这带来两个问题:
- 链接时的重定义问题,如果不加以处理,这些相同的实体会被链接器认为是重定义的符号,这违反了ODR(One Definition Rule)。对这个问题的主流解决方案是为模板实例化生成的实体添加特殊标记,链接器在链接时对有标记的符号做特殊处理。例如在 GNU 体系下,模板实例化生成的符号都被标记为弱符号(Weak Symbol)。不需要我们参与,连接器已经为我们解决了这个问题。
对于普通的函数和类,连接器是不会处理的。会报重定义的错误。
同时这个因为每一个编译单元都有实例化,会带来代码膨胀。
- 编译时长的问题,同一个模板传入相同实参在不同的编译单元下被实例化了多次,这是不必要的,浪费了编译的时间。
显示实例化的优点:
1. 降低编译器构建的时间
2.降低代码膨胀
3.针对发布lib,可以隐藏头文件
3. 特化(Specialization)
所谓特化,就是将泛型的东西搞得具体化一些,从字面上来解释,就是为已有的模板参数进行一些使其特殊化的指定,使得以前不受任何约束的模板参数,或受到特定的修饰(例如const或者摇身一变成为了指针之类的东东,甚至是经过别的模板类包装之后的模板类型,或者模版个数由多变少,由...Args变为,T 和 ...Args)或完全被指定了下来。
如果被完全的制定了下来,就叫做全特化,剩余的都叫做偏特化。
3.1. 特化与泛化
在之前的coding中, 我们可能并没有听说过什么特化还有泛化的概念,那么今天,我将来总结介绍这一“新”概念!泛化与特化的概念是一对反义词!所谓,
泛化:泛化其实就是泛型化(通用化)的意思,其实就是让你的代码通用性更高!更适合多种应用场景!
格式:(其实就是定义类模板/函数模板时候的代码格式啦~)
//下面是泛化版本的模板类 or 模板函数的definition//类模板definition
template<typename T1,typename T2,...typename Tn>
class templateClassName{
public://...
}
//函数模板的definition
template<typename T1,typename T2,...typename Tn>
void templateFuncName(Params){//...
}
特化:特化其实就是对于特殊的类型(类型模板参数)进行特殊的对待,给它开小灶,给它只适合它自己用的专用代码。特化又分为全特化和偏特化!
全特化:将类模板/函数模板的模板参数列表中的all模板参数做特殊化!
格式:(全特化时,类型模板参数列表为空!)
//下面是全特化版本的类模版 or 函数模版的definition
template<>
//全特化版本的类模版的definition
class templateClassName<给要特化的all模板类型参数do具体化>{
public://...
}
//全特化版本的模板函数的definition
template<>
void templateFuncName(具体的Params){//...
}
小细节之全特化的标识:template<>,看到这个标识时,你就要立马反应过来这里是在做全特化的工作(要记住wor~)
偏特化:将类模板/函数模板的模板参数列表中的部分模板参数做特殊化!
(那么为什么要进行偏特化呢?我认为,其实这就是因为偏特化会提高我模板代码的效率!你肯定是把其中经常用到的某种参数类型或者特殊的参数类型特化的,这样遇到这种模板参数适你的模板就可以进行特殊的代码处理了,进而可提高效率!否则你也不需要用到特化吧对吧?)
注意①:在写类模板/函数模板的特化版本前,必须要写出该模板的泛化版本!这一点务必要记住!(没有泛化版本根本就不可以写出特化版本的)
注意②:编译器会优先选择特化版本的类模板/函数模板的代码,也就是说,一旦你定义了一个类模板/函数模板的泛化版本以及对应的特化版本,如果你调用该类模板/函数模板时所传入的模板参数列表对应了特化版本的代码时,这时编译器就会优先将你特化版本的代码覆盖掉泛化版本的代码,进而使用特化版本的代码来执行程序~
注意③:
当类模板/函数模板进行全特化之后,这个全特化后的类/函数就不是一个模板类/函数了
(因为全特化完成后,模板参数类型为空了,即template<>了,就是一个具体的类/函数)
当类模板/函数模板进行偏特化之后,这个偏特化后的类/函数仍然是一个类模版/函数模版
(因为偏(局部)特化完成后,模板参数类型仍然不为空,即template,就还是一个模板类/函数)
但是同时注意的是,虽然全特化已经是没有模版参数化了,但是它仍然需要实例化的时候,调用到这个类,也就是生成类对象的时候,才会在汇编层面产生,对应的构造函数,和成员函数,如果不进行实例化的话,那么和泛化版本,和偏特话版本,没有任何的区别。
这也说明 特化和实例化是两个层面的事,特化还是提供一种特殊的模子,而最终是否生成代码,还要看是否能够进行实例化,只有实例化之后,才能生成真正的代码。(但是这里函数模版的全特化版本有点特殊,只要全特化了就会生成对应的函数)
3.2. 类模板特化
先给出类模板的泛化版本:
#include<iostream>
using namespace std;
template <typename T1,typename T2>
class A{
public:A(){}void Max(T1 t1,T2 t2){}T1 Aa;T2 Bb;
};
3.2.1 类模板全特化
3.2.1.1 常规全特化
废话不多说,请看以下代码:
#include<iostream>
using namespace std;
template <typename T1,typename T2>
class A{
public:A(){}void Max(T1 t1,T2 t2){}T1 Aa;T2 Bb;
};
template<>
class A<int,int>{public:A(){ }void Max(int t1,int t2){std::cout<<"i am 全特化版本,int,int"<<std::endl;}int Aa;int Bb;
};
int main(int argc,char *argv[]){A<int,int>temp;temp.Max(2,3);return 0;
}
运行结果如下:
我们来看下上面的代码中,对应的汇编代码:
那么如果我不进行调用(按需实例化),那么在汇编层面能否生成代码呢,我们看下,
结果是不会生成任何代码,这也说明了实例化和特化还是两个不同层面的概念。
3.2.1.2 特化类模板的成员函数而不是类模板本身
我们可以不特化类模板,而仅仅是去特化类模板的成员函数。这样,当你创建该类模板对象时,虽然是调用的泛化版本的构造函数和析构函数去创建和释放它,但是如果说你定义了该泛化版本的对象但你这个对象定义时所提供的模板参数与该特化成员函数的模板参数列表一致时,那么编译器就会优先调用该特化版本的成员函数(当你这个对象使用到该成员函数时候),而不是泛化版本的成员函数。
废话不多说,请看以下代码:
#include<iostream>
using namespace std;template <typename T1,typename T2>
class A{
public:A(){std::cout<<"i am general constructor"<<std::endl;}void Max(T1 t1,T2 t2){std::cout<<"i am general version"<<std::endl;}T1 Aa;T2 Bb;
};
template<>
void A<int,int>::Max(int t1,int t2){std::cout<<"i am full specification,int,int"<<std::endl;}int main(int argc,char *argv[]){A<int,int>temp;temp.Max(2,3);return 0;
}
3.2.2 类模板偏特化(局部特化)
3.2.2.1 从模板参数数量这个角度来进行偏特化
请看以下代码:
#include<iostream>
using namespace std;template <typename T1,typename T2>
class A{
public:A(){std::cout<<"i am general constructor"<<std::endl;}void Max(T1 t1,T2 t2){std::cout<<"i am general version"<<std::endl;}T1 Aa;T2 Bb;
};
template<typename T2>
class A<int,T2>{public:A(){std::cout<<"i am part specification constructor"<<std::endl;}void Max(int t1,T2 t2){std::cout<<"i am part specification Max"<<std::endl;}int Aa;T2 Bb;
};int main(int argc,char *argv[]){A<int,int>temp;temp.Max(2,3);return 0;
}
运行结果:
全特化的标识是template<>,表示all我模板参数我都特殊化了,而不是把all的模板参数都列出来!模板参数一旦在模板参数列表列出来,就表明这个模板参数我不想特化的意思!!!
3.2.2.2 从模板参数范围这个角度来进行偏特化
什么叫做参数范围呢?参数的范围既能缩小又能增大。
//举例:
//本来参数类型是int:
//参数范围缩小:
int->const int
int->int*
int->int&
int->int&&
int->const int*
//参数范围增大:
const int->int
int*->int
int&->int
int&&->int
了解了模板参数范围的概念后,下面就用参数的范围来do类模板的偏特化~
请看以下代码:
#include<iostream>
using namespace std;
template<typename T>
class TC {//泛化的TC类版本(带1个模板参数)
public:void testfunc() {cout << "泛化版本!" << endl;}
};
//从模板参数范围上进行的类模板的特化版本//告诉编译器,如果模板参数类型你传入的是const类型,那就优先用该特化版本的类模板代码
template<typename T>
class TC<const T> {//TC<const T>特化版本
public:void testfunc() {cout << "TC<const T>特化版本!" << endl;}
};
//告诉编译器,如果模板参数类型你传入的是指针类型,那就优先用该特化版本的类模板代码
template<typename T>
class TC<T*> {//TC<T*>特化版本
public:void testfunc() {cout << "TC<T*>特化版本!" << endl;}
};
//告诉编译器,如果模板参数类型你传入的是左值引用类型,那就优先用该特化版本的类模板代码
template<typename T>
class TC<T&> {//TC<T&>特化版本
public:void testfunc() {cout << "TC<T&>特化版本!" << endl;}
};
//告诉编译器,如果模板参数类型你传入的是右值引用类型,那就优先用该特化版本的类模板代码
template<typename T>
class TC<T&&> {//TC<T&&>特化版本
public:void testfunc() {cout << "TC<T&&>特化版本!" << endl;}
};
int main(void) {TC<const int> tcint;//调用TC<const T>特化版本的代码!tcint.testfunc();TC<const double> tcdouble;//调用TC<const T>特化版本的代码!tcdouble.testfunc();cout << "---------------------------" << endl;TC<int*> tcintPoint;//调用TC<T*>特化版本的代码!tcintPoint.testfunc();TC<double*> tcdoublePoint;//调用TC<T*>特化版本的代码!tcdoublePoint.testfunc();cout << "---------------------------" << endl;TC<int&> tcintAnd;//调用TC<T&>特化版本的代码!tcintAnd.testfunc();TC<double&> tcdoubleAnd;//调用TC<T&>特化版本的代码!tcdoubleAnd.testfunc();cout << "---------------------------" << endl;TC<int&&> tcintAndAnd;//调用TC<T&&>特化版本的代码!tcintAndAnd.testfunc();TC<double&&> tcdoubleAndAnd;//调用TC<T&&>特化版本的代码!tcdoubleAndAnd.testfunc();return 0;
}
输出的结果如下:
4. 对比表格
-
概念 实例化(Instantiation) 特化(Specialization) 目的 生成具体类型的模板代码 为特定类型定制模板行为 是否生成新代码 是 不生成,需要实例化才能触发 语法 隐式:生成对象,调用成员函数
显式:
template class Name<Type>
template<> class Name<Type>
实例化方式 隐式实例化 显式实例化 触发条件 首次使用模板 手动声明 template class ...
编译时间影响 可能增加编译时间 可优化编译时间 代码位置 任何使用模板的地方 通常在 .cpp
文件中