模板详细介绍
泛型编程
我们以一个交换函数为例,我们已经知道了C++最重要的一个知识之一就是支持了函数重载,这可以让我们对于不同数据类型的交换共同使用一个函数名。
void Swap(int& left, int& right)
{int temp = left;left = right;right = temp;
}void Swap(double& left, double& right)
{double temp = left;left = right;right = temp;
}void Swap(char& left, char& right)
{char temp = left;left = right;right = temp;
}
使用函数重载虽然可以实现,但是有一下几个不好的地方:
- 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数
- 代码的可维护性比较低,一个出错可能所有的重载均出错
那么有没有一个方法可以让我们只用一个函数模板就能应对不同类型的交换函数,答案是有的,就是“函数模板”。看名称就知道是自己创建一个模板,让编译器根据不同的类型利用该模子来生成代码。
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
而模板又分为函数模板和类模板
函数模板
函数模板概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
函数模板格式
template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表){}
//template<class T>
template<typename T>
void Swap(T& left, T& right)
{T temp = left;left = right;right = temp;
}int main()
{int i = 1, j = 2;Swap(i, j);double x = 1.1, y = 2.2;Swap(x, y);return 0;
}
注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)
注意调用Swap函数时,调用的不是同一个函数,首先,指令不一样,因为temp的大小不一样。我们看一下汇编语言就知道是2个不同的函数。
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。
函数模板的实例化
- 隐式实例化:让编译器根据实参推演模板参数的实际类型
- 显式实例化:在函数名后的<>中指定模板参数的实际类型
模板参数匹配原则
- 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
// 专门处理int的加法函数
int Add(int left, int right)
{return left + right;
}// 通用加法函数
template<class T>
T Add(T left, T right)
{return left + right;
}void Test()
{Add(1, 2); // 与非模板函数匹配,编译器不需要特化Add<int>(1, 2); // 调用编译器特化的Add版本
}
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板。
// 专门处理int的加法函数
int Add(int left, int right)
{return left + right;
}// 通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{return left + right;
}void Test()
{Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
}
我们有些小伙伴们可能想知道如果2个参数类型不一样会是什么结果。先说结论:
- 从下面的代码可以知道,模板函数不允许自动类型转换,但普通函数可以进行自动类型转换。
template<class T>
T Add(const T& left, const T& right)
{return left + right;
}int Add(int left, int right)
{return left + right;
}
int main()
{cout << Add(1, 2) << endl;//没有普通Add函数会报错,有普遍函数会调用普通函数,并自动类型转换//cout << Add(1.1, 2) << endl; //报错//修改方法cout << Add((int)1.1, 2) << endl; //强制类型转换cout << Add(1.1, (double)2) << endl;cout << Add<int>(1.1, 2) << endl; //显示实例化cout << Add<double>(1.1, 2) << endl;return 0;
}
报错的原因是:因为在编译期间,当编译器看到该实例化时,需要推演其实参类型通过实参1.1将T推演为double,通过实参1将T推演为int类型,但模板参数列表中只有一个T,编译器无法确定此处到底该将T确定为int 或者 double类型而报错。
类模板
类模板的定义格式
template<class T1, class T2, ..., class Tn>
class 类模板名
{// 类内成员定义
};
我们以栈为例
//typedef char DataType;
template<class T> //把DataType改成T
class Stack
{
public:Stack(size_t capacity = 4){cout << "Stack()" << endl;_array = new T[capacity];_capacity = capacity;_size = 0;}void Push(const T& data){// CheckCapacity();_array[_size] = data;_size++;}~Stack(){cout << "~Stack()" << endl;delete[] _array;_array = nullptr;_size = _capacity = 0;}
private:T* _array;int _capacity;int _size;
};int main()
{Stack<int> st1; //int类型Stack<double> st2; //double类型return 0;
}
当然st1和st2是2个不同的栈
类模板的实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>
,然后将实例化的类型放在<>
中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
非类型模板参数
模板参数分类类型形参与非类型形参。
类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
比如说我们定义一个静态数组,以前是不是这样定义。
#define N 10
template<class T>
class arr
{
private:T _arr[N];
};int main()
{arr<int> a1;arr<double> a2;return 0;
}
非类型形参:就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
现在要求a1和a2分别是10和10000的大小空间,那么上面的方法是没法实现,要么都是10个空间或者10000个空间大小。所以C++模板中有了非类型模板参数,专门用来设置数组的大小。
template<class T,size_t N>
class arr
{
private:T _arr[N];
};int main()
{arr<int,10> a1;arr<double,10000> a2;return 0;
}
a1这个对象,传入参数时,第一个参数是int,第二个参数是10,于是a1就初始化为10个大小空间的int类型数组。同理,a2就是10000个空间大小的double类型数组。
注意
- 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
- 非类型的模板参数必须在编译期就能确认结果。
模板的特化
概念
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板.
template<class T>
bool Less(T left, T right)
{return left < right;
}
int main()
{cout << Less(1, 2) << endl; // 可以比较,结果正确int* p1 = 1;int* p2 = 2;cout << Less(p1, p2) << endl; // 可以比较,结果错误return 0;
}
上述示例中,我们想要比较1和2的大小,但是Less内部并没有比较p1和p2指向的对象内容,而比较的是p1和p2指针的地址,这就无法达到预期而错误。此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式
模板特化中分为函数模板特化与类模板特化
函数模板特化
函数模板的特化步骤:
- . 必须要先有一个基础的函数模板
- 关键字template后面接一对空的尖括号<>
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型
- 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误
//基础模板
template<class T>
bool Less(T left, T right)
{return left < right;
}//模板特化
template<>
bool Less<int*>(int* left, int* right)
{return *left < *right;
}int main()
{int x = 1;int y = 2;int* p1 = &x;int* p2 = &y;cout << Less(1, 2) << endl; //基础模板cout << Less(p1, p2) << endl; //模板特例化return 0;
}
其中一个是基础模板,一个是特例化。首先,我们将T
特化为了int*
,此时将T
从模板参数列表中抽离出来,改为int*
放到函数名Less
后面,用尖括号括起来,然后把函数参数中所有的T改为特化后的int*
。这样当遇到指针类型的时候,就直接调用模板特化,而不在走基础模板。
注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。
不过对函数模板进行特化,还不如给类模板进行特化,模板特化在类模板中比较实用。所以想给函数进行特化,就直接重载一个函数,简单明了。
bool Less(Date* left, Date* right)
{return *left < *right;
}
类模板特化
语法跟函数模板特化一样,类模板特化分为全特化和偏特化。
全特化:全特化即是将模板参数列表中所有的参数都确定化。
//基础模板
template<class T1, class T2>
class Data
{
public:Data() { cout << "Data<T1, T2>" << endl; }
private:T1 _d1;T2 _d2;
};//模板特化
template<>
class Data<double, int>
{
public:Data() { cout << "Data<double, int>" << endl; }
private:double _d1;int _d2;
};int main()
{Data<int, int> d1; //Data<T1, T2>Data<double, int> d2; //Data<double, int>return 0;
}
此时T1和T2
都是全特化,调用基础模板。当第一个参数为double
,第二个参数为int
时,调用模板特化。像这样Data<double, int> d2;
,如果参数类型不匹配的话,就会调用基础模板。
偏特化
偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。比如对于以下模板类:
template<class T1, class T2>
class Data
{
public:Data() { cout << "Data<T1, T2>" << endl; }
private:T1 _d1;T2 _d2;
};
偏特化有以下两种表现方式:
- 部分特化:将模板参数类表中的一部分参数特化
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;
};
//两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:Data(const T1& d1, const T2& d2): _d1(d1), _d2(d2){cout << "Data<T1&, T2&>" << endl;}
private:const T1& _d1;const T2& _d2;
};void test2()
{Data<double, int> d1; // 调用特化的int版本Data<int, double> d2; // 调用基础的模板 Data<int*, int*> d3; // 调用特化的指针版本Data<int&, int&> d4(1, 2); // 调用特化的指针版本
}
类模板特化应用示例
#include<vector>
#include <algorithm>template<class T>
struct Less
{bool operator()(const T& x, const T& y) const{return x < y;}
};int main()
{Date d1(2022, 7, 7);Date d2(2022, 7, 6);Date d3(2022, 7, 8);vector<Date> v1;v1.push_back(d1);v1.push_back(d2);v1.push_back(d3);// 可以直接排序,结果是日期升序sort(v1.begin(), v1.end(), Less<Date>());vector<Date*> v2;v2.push_back(&d1);v2.push_back(&d2);v2.push_back(&d3);// 可以直接排序,结果错误日期还不是升序,而v2中放的地址是升序// 此处需要在排序过程中,让sort比较v2中存放地址指向的日期对象// 但是走Less模板,sort在排序时实际比较的是v2中指针的地址,因此无法达到预期sort(v2.begin(), v2.end(), Less<Date*>());return 0;}
通过观察上述程序的结果发现,对于日期对象可以直接排序,并且结果是正确的。但是如果待排序元素是指针,结果就不一定正确。因为:sort最终按照Less模板中方式比较,所以只会比较指针,而不是比较指针指向空间中内容,此时可以使用类版本特化来处理上述问题:
// 对Less类模板按照指针方式特化
template<>
struct Less<Date*>
{bool operator()(Date* x, Date* y) const{return *x < *y;}
};
特化之后,再运行上述代码,就可以得到正确的结果。