在前面的文章中,我们已经简单的了解了模板的使用,在这篇文章中,我们将继续深入探讨模板
1.模板的特化
1.1 概念
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现一个专门用来进行小于比较的函数模板
#include <iostream>
using namespace std;
template <class T>
bool LESS(T left, T right)
{
return left < right;
}
class date
{
public:
date(int year =2025,int month=2,int day=1)
:_year(year)
,_month(month)
,_day(day)
{}
bool operator<(const date& d)const
{
return (_year < d._year) ||
(_year == d._year && _month < d._month) ||
(_year == d._year && _month == d._month && _day < d._day);
}
bool operator>(const date& d)const
{
return (_year > d._year) ||
(_year == d._year && _month > d._month) ||
(_year == d._year && _month == d._month && _day > d._day);
}
private:
int _year;
int _month;
int _day;
};
int main()
{
cout << LESS(1, 2) << endl; // 可以比较,结果正确
cout << LESS(2, 1) << endl; // 可以比较,结果正确
//日期类(自定义类型)也可以
date d1(2025, 4, 20);
date d2(2025, 1, 7);
cout << LESS(d1, d2) << endl;
return 0;
}
但是,倘若我们要这样比较,结果就不一定正确了,因为这里我们在比较地址,而不是比较内容本身!
因此,我们要对模板进行特化, 使得上述例子得以正确执行!
1.2 函数模板特化
函数模板的特化步骤:
1. 必须要先有一个基础的函数模板
2. 关键字template后面接一对空的尖括号<>
3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的 错误。
template <class T>
bool LESS(T left, T right)
{
return left < right;
}
template<>
bool LESS<date*>(date* left, date* right)
{
return *left < *right;
}
int main()
{
date d1(2050, 4, 20);
date d2(2022, 1, 7);
date* p1 = &d1;
date* p2 = &d2;
cout << LESS(p1, p2) << endl;
return 0;
}
这样,那个指针的例子便可以通过了!因为其将调用特化之后的版本,而不走原来的模板了!
但是,我们之前就学过函数,这样在写一个函数模板,显得多此一举了,不如直接写一个函数,还省事!
bool LESS(date* d1, date* d2)
{
return *d1 < *d2;
}
int main()
{
date d1(2002, 4, 20);
date d2(2022, 1, 7);
date* p1 = &d1;
date* p2 = &d2;
cout << LESS(p1, p2) << endl;
return 0;
}
结论:函数模板不建议特化。
1.3 类模板特化
1.3.1 全特化
全特化即是将模板参数列表中所有的参数都确定化
//总的模板
template <class T1,class T2>
class rens
{
public:rens() { cout << "rens<t1,t2>" << endl; }
private:
T1 _r1;
T2 _r2;
};
//全特化
template<>
class rens<int*, int*>
{
public:rens() { cout << "rens<int*,int*>" << endl; }
};
template<>
class rens<int, char>
{
public:rens() { cout << "rens<int, char>" << endl; }
//这个私有写不写都行,个人不喜欢写
private:
int r1;
char r2;
};
template<>
class rens<char, int>
{
public:rens() { cout << "rens<char,int>" << endl; }
//这个私有写不写都行,个人不喜欢写
private:
char r1;
int r2;
};
int main()
{
rens<int, char> r1;
rens<int*,int> r2;
rens<char, int> r3;
rens<int*, int*> r4;
return 0;
}
运行结果如下:
结果是这个的原因也不难理解,编译器在选择模板来编译时,一定回去选择最合适的模板来匹配编译!
1.3.2 偏特化
偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。
其有两种表现方式:一种是将模板参数类表中的一部分参数特化(见第一个偏特化例子),另一种是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
template<class t1,class t2>
class rens
{
public:
rens() { cout << "rens<t1,t2>" << endl; }
private:
t1 _d1;
t2 _d2;
};
//偏特化,将模板参数类表中的一部分参数特化
template<class T1>
class rens<T1, int>
{
public:
rens()
{
cout << "rens<T1,int>" << endl;
}
//这个东西写不写都行
private:
T1 _d1;
int _d2;
};
//两个参数偏特化为指针类型
template<class T1,class T2>
class rens<T1*, T2*>
{
public:
rens() { cout << "rens<T1*, T2*>" << endl; }
};
//两个参数偏特化为引用类型
template<class T1,class T2>
class rens<T1&, T2&>
{
public:
rens() { cout << "rens<T1&, T2&>" << endl; }
};
//两个参数偏特化为指针类型和引用类型
template<class T1, class T2>
class rens<T1*, T2&>
{
public:rens() { cout << "rens<T1*,T2&>" << endl; }
};
int main()
{
rens<double, int> r1;
rens<int, double> r2;
rens<int*, int*> r3;
rens<int&, int&>r4;
rens<int*, int&>r5;
return 0;
}
运行结果:
2.模板分离编译
与普通函数不同,带有模板参数的函数的声明和定义不能分开!否则会报错!!
为此,我们的解决方案是将带模板参数的函数的声明的定义都放在.h文件中,如下图所示:
3.应用
比如我现在有一个vector容器,现在我们想写一个函数来打印它,那应该怎么办呢?我们有如下几种方式:
一、老老实实的写打印函数
#include<vector>
#include<list>
void print(const vector<int>& v)
{
vector<int>::const_iterator it = v.begin(); //参数是const vector<int>,所以我们的迭代
器也要用const_iterator
while (it != v.end())
{
cout << *it << " " ;
++it;
}
cout << endl;
}
int main()
{
vector<int> v1 = { 1,2,3,4,5,6 };
print(v1);
return 0;
}
这样写优缺点也很明确,优点是这种写法很简单,看过几遍就会写,缺点是过于死板,如果再让你打印一下double类型或者其他类型,你还得辛辛苦苦去写,太浪费时间,(不过可以适用于摸鱼哈,开玩笑请勿当真!) 为此,我们可以在函数里面加一丢丢模板,这样我们就不必在写double的打印函数了!
二、加模板参数的打印函数
#include<vector>
template <class T>
void print(const vector<T>& v)
{
// typename vector<T>::const_iterator it = v.begin();
//解释一下这里为什么要加上typename:
// 因为vector<T>是一个模板类,
// 编译器可能无法自动推断出vector<T>::const_iterator是一个类型,
// 因此需要typename明确指出
auto it = v.begin(); //推荐用auto,让编译器自动给我们推导类型
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
int main()
{
vector<int> v1 = { 1,2,3,4,5,6 };
print(v1);
vector<double> v2 = { 2.2,5.6,9.6,8.3,4.5 };
print(v2);
return 0;
}
这里有两种方法,一个是用typename关键字,另外一个就是引入c++11的auto类型, 使用auto更加便捷和简洁!
现在,你已经可以打印各种类型的vector了,但是,倘若现在突然让你打印list容器呢?而且我们来不及挨个写了,为此我们再改造一下,使之得以实现!
三、设置一个容器模板
#include<vector>
#include<list>
template<class container>
void print(const container& con)
{
//typename container::const_iterator it = con.begin();
auto it = con.begin();
while (it != con.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
int main()
{
vector<int> v1 = { 1,2,3,4,5,6 };
print(v1);
vector<double> v2 = { 2.2,5.6,9.6,8.3,4.5 };
print(v2);
list<int> l1 = { 1,5,6,9,8,7 };
print(l1);
list<double> l2 = { 1.1,2.2,3.3 };
print(l2);
return 0;
}
这样,不论我是vector<int>,还是list<double>,我都可以通过container模板将其替代回去,完成打印的操作!
4. 模板总结
【优点】
1. 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
2. 增强了代码的灵活性
【缺陷】
1. 模板会导致代码膨胀问题,也会导致编译时间变长
2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误