再探模板与泛型编程
本章目标
0.typename的特殊使用场景
1.非类型模板参数
2.模板特化
3.模板分离编译
0.typename的特殊使用场景
在定义模板参数的时候,我们会可以用class定义,也可以用typename定义.
这两者在此时的使用并没有什么区别
而在去模板的内嵌类型的时候,我们必须加上typename来表示,它是模板的内嵌类型
template <class container>
void print(const container& con)
{typename container::const_iterator it = con.cbegin();while (it != con.cend()){cout << *it << " ";it++;}cout << endl;}
如果我们不加typename提前声明它是模板的内嵌类型的话,编译器并不知道它是什么,如果当前的这个const_iterator,它可能会被认为是静态成员变量,那么此时就是不符合语法的.
1.非类型模板参数
在前面我们了解到模板支持泛型编程,我们template来定义模板,用class或者typename来定义模板参数,这种定义参数我们管它叫做类型形参,我们也可以定义一个常量在模板参数列表当中,这样的参数,我们管它叫做非类型模板参数.
template<class T, size_t N = 10>class array{public:T& operator[](size_t index){return _array[index];}const T& operator[](size_t index)const{return _array[index];}size_t size()const{return _size;}bool empty()const{return 0 == _size;}private:T _array[N];size_t _size;};
这样的好处是它很大程度上代替了宏,我们知道,因为宏本身带来的危险,在c++中是尽量去避开宏的使用,之前有介绍过用inline函数去代替宏定义的函数,而在这里同样的,因为宏本身的缺陷,我们并不考虑使用.
这样用非类型模板参数的好处是我可以随时调节N的大小
array<int,10> s1;
array<double ,100> s2;
我们在这里定义两个array类的对象,它们的容量分别是10和100.
如果我们使用宏的话,我们仅能定义一种大小的对象.
2.模板特化
在模板中,我们可以写出与类型无关的代码,来进行泛型编程.
但是在有些情况下,因为特殊的类型的原因,可能会出错
class Date
{
public:Date(int year = 1900, int month = 1, 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);}friend ostream& operator<<(ostream& _cout, const Date& d);
private:int _year;int _month;int _day;
};ostream& operator<<(ostream& _cout, const Date& d)
{_cout << d._year << "-" << d._month << "-" << d._day;return _cout;
}
template<class T>bool Less(T left, T right){return left < right;}int main(){cout << Less(1, 2) << endl; Date d1(2022, 7, 7);Date d2(2022, 7, 8);cout << Less(d1, d2) << endl; // 可以比较,结果正确
}Date* p1 = &d1;Date* p2 = &d2;cout << Less(p1, p2) << endl; // 可以比较,结果错误
return 0;
在这种情况下,我们实现了小于逻辑的函数,还有一个重载了<运算符的日期类,在前两个案例中,我们正常实现是没有任何问题的.它会按照日期的大小排序.
但是在第三个,我们用less来进行比较两个地址,那么它们的结果一定是随机的.
因为函数,我们写的是一个传值操作.比较两个地址的值.
2.1函数模板特化
我们可以可以模板特化一个date*类型的函数解决
以下是函数模板特化的步骤
- 必须要先有一个基础的函数模板
- 关键字template后面接一对空的尖括号<>
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型
- 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
它很像我们之前写过的函数模板的显示实例化.但跟其是不同的.显示实例化的它的底层实现是相同的.模板特化正是因为它的底层在当前类型没法使用了,我们需要在实现出一份
template<>
bool Less<Date*>( Date* p1, Date* p2)
{return *p1 < *p2;
}
但是对于函数模板特化要注意的情况是在参数列表中出现了const的情况
template <class T>
bool Less(const T& p1, const T& p2)
{return p1 < p2;
}
template<>
bool Less<Date*>( Date*const& p1, Date* const& p2)
{return *p1 < *p2;
}
在正常模板中的const修饰的是p1和p2本身不能够修改.
而在特化模板中,我们不能把const放到*的左边那样是表示指针指向的对象不能够修改.
我们想要的是指针本身不能被修改.所以要放到指针的右边.
不过我们对于函数模板特化,我们并不建议使用,我们可以另写一个函数,让它与模板构成函数重载
bool Less(Date* left, Date* right)
{return *left < *right;
}
2.2类模板特化
类模板特化是分为两种一种是全特化,另一种是偏特化.
template<class T1, class T2>
class Data{public:Data() {cout<<"Data<T1, T2>" <<endl;}private:T1 _d1;T2 _d2;};template<>
class Data<int, char>{public:Data() {cout<<"Data<int, char>" <<endl;}private:int _d1;char _d2;};void TestVector(){Data<int, int> d1;Data<int, char> d2;}
我们可以指定类的参数.来另外实现一个类.
2.2.1偏特化
偏特化分为两种,第一种是偏特化是我们只特化部分参数
template <class T1>
class Data<T1, int>{public:Data() {cout<<"Data<T1, int>" <<endl;}private:T1 _d1;int _d2;};
我们还可以对参数进行限制,例如特化的模板参数全都是指针
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:Data() {cout<<"Data<T1*, T2*>" <<endl;}private:T1 _d1;T2 _d2;};
在后面的priority_queue的模拟实现,我们会进一步进行解释.
3.模板分离编译
在前面我们介绍模板一般情况下我们是不会进行模板分离编译的,
分离编译会出现链接错误.
为了解释是什么链接错误,让我们先回顾一下编译型语言生成可执行程序的过程.
1.预处理
展开头文件,宏替换,条件编译.去掉注释.
2.编译
检查语法,将代码转为汇编语言
3.汇编
将汇编语言转为二进制机器码
4.链接
将它们之前的链接在一起,生成可执行程序
在链接前,各个文件是不会有交互的,如果我们将模板进行声明定义分离,就会导致模板不会实例化对象
我们举个例子
//test.h
#pragma once
#include<iostream>
#include<string>
#include<vector>
#include<list>
using namespace std;
template <class container>
void print(const container& con);
//test.cpp
#include"test.h"
template <class container>
void print(const container& con)
{typename container::const_iterator it = con.cbegin();while (it != con.cend()){cout << *it << " ";it++;}cout << endl;
}
#include"test.h"
int main()
{/*Student s1;s1.Print();*/vector<int> v = { 1,2,34,5 };print(v);return 0;
}
在主函数中只有声明并且知道模板应该实例化成什么,但没有定义.
而在test.cpp的文件中,知道模板的定义,但没有实例化,因为不知道模板参数的类型
因为实例化一定实在链接之前的阶段完成的.在连接时,当主函数根据函数声明去其他文件的符号表中找函数定义,因为没有实例化,也就没有定义.
这样就会报链接错误.
当然也是有解决办法的就是显示实例化出一份模板
template
void print<vector<int>>(const vector<int>& con);
在test.cpp的文件中就解决了.
但我们仍然建议在头文件中定义模板,因为这样头文件展开之后,模板就知道实例化成什么了,因为在同一个文件当中.这样也就不会出现链接错误了.