首先强调一句话:模板特例化首要目标是保证编译通过!!!
一、为什么需要模板特化
这周四,帮一个帅小伙儿排查问题,他写了一段类似于这样的逻辑:
class student{
int age = 25;
public:
void mycout(){
cout<<age<<endl;
}
};
template<typename T>
class Base
{
T t;
public:
Base(T t):t(t){}
void display(){
if(1==0){
cout<<t<<endl;
}
}
};
int main() {
student stu;
Base<student> base(stu);
base.display();
}
看起来能编译通过,因为cout<<t<<endl,也就是cout<<stu<<endl这个不合理的逻辑永远不会被执行。但是,这个是编译不过的。
因为实例化后,编译器会二次编译实例化后的代码,cout<<stu这种行为,在二次编译的过程中会被检查出来有语法错误。
有个有意思的事情,去掉base.display();后,编译不会报错。根据《C++ primer》中的解释,对于类模板中的成员函数,即使在类模板实例化以后,如果这个函数不被调用,那么也不会被具体化。
言归正传,这个问题的解决方案就是模板特化!!!
二、类模板特例化
我们定义类模板的时候,一定要考虑它可能被实例化成哪些类型。假如这个类模板有可能被实例化的方式有100种,其中99种是通用的,但是有一种实例化方式,实例化后是编译不过的。可以采用模板特化的方法,单独把这种类型拎出来,告诉编译器,碰到这种特殊的类型,按照特殊的方式展开模板。
看代码:
template<typename T>
class Base
{
T t;
public:
Base(T t):t(t){}
void display(){
cout<<t<<endl;
}
};
template<>
class Base<student>
{
student stu;
public:
Base(student s):stu(s){}
void display(){
stu.mycout();
}
};
int main() {
student stu;
Base<student> base1(stu);
base1.display();
Base<int> base2(12);
base2.display();
Base<string> base3("abc");
base3.display();
Base<char> base4('a');
base4.display();
}
当你将模板参数实例化为student时候,模板类就会按照特例化版本展开。对于其它类型,就会按照通用版本展开。
看到这里你可能有个疑问,特例化版本的模板,必须和通用模板类成员上保持一致吗?答案是否定的。还是那句话,编译器只是负责将模板展开,你只要保证展开后没有语法错误就可以了。
template<typename T>
class Base
{
T t;
public:
Base(T t):t(t){}
void display(){
cout<<t<<endl;
}
};
template<>
class Base<student>
{
public:
void display(){
cout<<"1233211234567"<<endl;
}
void f(){}
};
int main() {
Base<student> base1;
base1.display();
}
正常输出。
三、继承关系下的模板特例化
但是,帅小伙儿想要特例化的模板,是一个继承于基类类的类模板,这就要多考虑考虑。看代码:
class student{
int age = 25;
public:
void mycout(){
cout<<age<<endl;
}
};
class Base
{
int a;
public:
Base(int t):a(t){}
void display(){
cout<<a<<endl;
}
};
template<typename T>
class Derived : public Base
{
T t;
public:
Derived(T t):Base(123),t(t){}
void display(){
Base::display();
cout<<t<<endl;
}
};
template<>
class Derived<student> : public Base
{
public:
Derived():Base(123){}
void display(){
Base::display();
cout<<"te li hua"<<endl;
}
};
int main() {
Derived<student> d;
d.display();
}
运行结果:
123
te li hua
正如前文所说,特例化版本没必要和通用版本成员保持一致,所以我故意给特例化版本写了一个无参构造函数。
实际上,那个帅小伙儿写的基类里面,有纯虚函数,继承的时候还必须重写。我们不禁想到一个问题:通用版本类模板是派生类,那么它的特例化版本也必须是派生类吗?
答案是否定的,还记得我开篇强调的那句话吗?看代码:
class student{
int age = 25;
public:
void mycout(){
cout<<age<<endl;
}
};
class Base
{
int a;
public:
Base(int t):a(t){}
void display(){
cout<<a<<endl;
}
};
template<typename T>
class Derived : public Base
{
T t;
public:
Derived(T t):Base(123),t(t){}
void display(){
Base::display();
cout<<t<<endl;
}
};
template<>
class Derived<student>
{
student stu;
public:
Derived(student stu):stu(stu){}
void display(){
stu.mycout();
cout<<"te li hua"<<endl;
}
};
int main() {
student stu;
Derived<student> d(stu);
d.display();
Derived<int> d1(456);
d1.display();
}
运行结果:
25
te li hua
123
456
为什么没问题呢?因为特例化就是想让编译器编译没有问题。当编译器看到 Derived d(stu);的时候,自然按照特化版本去展开编译。展开结果没问题就可以了,至于说继承关系变了毫无影响,编译器只管你能不能编译过,至于你继承谁,编译器才不管呢。因为理论上按照通用类模板实例化展开的各个类,彼此之间也是没有联系的。
你甚至可以玩个骚操作,特例化版本继承自其它父类:
class student{
int age = 25;
public:
void mycout(){
cout<<age<<endl;
}
};
class Base
{
int a;
public:
Base(int t):a(t){}
void display(){
cout<<a<<endl;
}
};
template<typename T>
class Derived : public Base
{
T t;
public:
Derived(T t):Base(123),t(t){}
void display(){
Base::display();
cout<<t<<endl;
}
};
class Test{
public:
void display(){
cout<<"I am Test class"<<endl;
}
};
template<>
class Derived<student> : public Test
{
student stu;
public:
Derived(student stu):stu(stu){}
void display(){
stu.mycout();
cout<<"te li hua"<<endl;
Test::display();
}
};
int main() {
student stu;
Derived<student> d(stu);
d.display();
Derived<int> d1(456);
d1.display();
}
运行结果:
25
te li hua
I am Test class
123
456
还是那句话,你特例化了,编译器就按照特例化方式展开。编译器才不管继承自谁呢。