1.泛型编程和模板的基本介绍
所谓泛型编程就是以独立于任何特定类型的方式编写代码。而模板是泛型编程的基础。模板分为函数模板和类模板,模板是创建类或函数的蓝图或公式。
我们来看一个需求:编写一个函数对两个数的大小进行比较?
int compare(const int& a, const int& b)
{
if (a > b) return 1;
if (b > a) return -1;
return 0;
}
int compare(const double& a, const double& b)
{
if (a > b) return 1;
if (b > a) return -1;
return 0;
}
int main()
{
cout << compare(1, 2) << endl;
cout << compare(1.1, 2.2) << endl;
return 0;
}
输出结果
-1
-1
如果这样实现两个数的比较大小,虽然C++的函数重载支持这样的操作,但是这样会有很大问题,如果我要比较不是int,double这样类型的数据,那么我就要补充更多这样类似的函数。但是我们发现这些函数几乎相同,它们之间唯一的区别是形参的类型不同。
我们可以通过函数模板来优化代码!
2.函数模板
函数模板格式:
template<typename T1, typename T2,......,typename Tn>
返回值类型函数名(参数列表){}
模板定义以关键字 template 开始,后接模板形参表,模板形参表是用尖括号括住的一个或多个模板形参的列表,形参之间以逗号分隔。模板形参表不能为空。
//注意函数模板将typename换成class也是可以的但是不能用struct
template<class T>
int compare(const T& a,const T& b)
{
if (a > b) return 1;
if (b > a) return -1;
return 0;
}
int main()
{
cout << compare(1, 2) << endl;
cout << compare(1.1, 2.2) << endl;
return 0;
}
输出结果
-1
-1
-1
使用函数模板时,编译器会推断哪个(或哪些)模板实参绑定到模板形参。一旦编译器确定了实际的模板实参,就称它实例化了函数模板的一个实例。比如:调用compare函数,编译器会根据传入的类型(这里两个调用int,double)实例化成int版本,和double版本。编译器通过函数模板,为我们承担了编写重复类型函数的单调工作。
其实,在人类发展的过程中,使用机器来替代人工是一种很大的进步,使用模板编写泛型编程,让编译器做重复的操作,又何尝不是一种进步呢!
3.函数模板的实例化
一旦编译器确定了实际的模板实参,就称它实例化了函数模板的一个实例。函数的实例化分为:隐式实例化和显式实例化。
隐式实例化:就是让编译器根据实参推演模板参数的实际类型;显式实例化:手动在函数名后的<>中指定模板参数的实际类型。
对于隐式的实例化要保证传入的实参类型正确:比如:两个数相加求和?
template<class T>
T add(const T& a, const T& b)
{
return a + b;
}
int main()
{
int a = 1;
double b = 2.2;
cout << add(a,b) << endl;
return 0;
}
使用隐式实例化编译器无法推导,T是什么类型,编译会报错。我们可以通过显示的实例化来告诉编译器,我们想要的类型,编译器会尝试去进行隐式类型转换!
int main()
{
int a = 1;
double b = 2.2;
cout << add<int>(a,b) << endl;
return 0;
}
输出结果:
3
4.类模板
和函数模板一样,类也可以定义为模板。
我们使用一个更为实用的例子queue的模拟实现来使用类模板。队列是先进先出的一种数据结构,在尾部插入数据,在头部删除数据,链表的头删尾插的效率是O(1),可以使用list来实现,但是STL默认使用deque来作为基础数据结构实现队列(适配器模式实现队列),关于deque的底层实现是一个双端队列,实现起来是比较复杂的。
1.queue.h头文件:
#pragma once
#include<deque>
namespace xiYan
{
template<class T,class continer = deque<T>>
class queue
{
public:
void push(const T& val) { _con.push_back(val); }
T& front() { return _con.front(); }
void pop() { _con.pop_front(); }
bool empty() { return _con.empty(); }
private:
continer _con;
};
}
2.test.cpp使用自定义的queue
#include<iostream>
#inlcude<list>
using namespace std;
#include"queue.h"//注意queue.h头文件中使用deque没有展开<deque>,注意要展开std!
using namespace xiYan;
int main()
{
//指定一个list容器也是可以的,但是vector不行,模拟实现使用的有些接口vector是没有的,比如头删:由于效率问题vector直接没有支持。
//queue<int, list<int>>q;
queue<int> q;
q.push(1);
q.push(2);
while (!q.empty())
{
cout << q.front() << " ";
q.pop();
}
return 0;
}
类模板和函数模板一样,也是template关键词,后接模板形参表。不同于函数模板的是,类模板必须显示的实例化。
当然在模板中还可以再去定义模板:比如,在类模板中在定义函数模板。
template<class T>
class A
{
public:
template<class T>
T sum(T a, T b)
{
return a + b;
}
T minus(T a, T b)
{
return a - b;
}
};
int main()
{
A<int> a;
cout << a.sum(2.2, 1.1) << endl;
cout << a.minus(2, 1) << endl;
return 0;
}
输出结果:
3.3
1
调用sum函数时会匹配double类型,在不同的模板中命名可以重复,不过这样在函数模板,参数列表中在参数名再命名为T是不规范的,要避免命名的重复。
但是在同一个模板中命名是不能重复的。
template<class T,class T>//错误
class A{};
5.在模板定义内部指定类型
如果我们要定义一个通用的打印函数,传入容器对象,使用迭代器怎么打印呢?
template<class Continer>
void Print(Continer& v)
{
Continer::const_iterator it = v.begin();
while (it != v.end())
{
cout << *it << " ";
++it;
}
}
要接受一个返回类型v.begin(),就要在模板内部定义一个类型,但是这里会有歧义,就是说const_iterator可以是静态的全局变量,也可以是一个类型(如:typedef一个类型),那么默认情况下编译器认为const_iterator是一个数据成员。我们完全可以定义这样的代码:
class Continer
{
public:
static int const_iterator;
};
int Continer::const_iterator = 1;
int main()
{
Continer::const_iterator = 5;
return 0;
}
所以我们要使用typename显示的指定一个类型。
template<class Continer>
void Print(Continer& v)
{
typename Continer::const_iterator it = v.begin();
……
}
6.非类型模板形参
模板形参不必都是类型,也可以是非类型形参。前面我们所说的由typename,class引起的是类型形参,而非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
下面是C++11增加的array类的模拟实现。
//array.h
#pragma once
namespace xiYan
{
template<class T,size_t n>
class array
{
public:
T& operator[](size_t pos) { return _arr[pos]; };
const T& operator[](size_t pos) const { return _arr[pos]; }
size_t size() { return _size; };
bool empty() { return _size == 0; }
protected:
T _arr[n];
size_t _size;
};
}
//test.cpp
#include"array.h"
using namespace xiYan;
int main()
{
array<int, 10> arr;
return 0;
}
7.回顾compare函数
template<class T>
int compare(const T& a,const T& b)
{
if (a > b) return 1;
if (b > a) return -1;
return 0;
}
我们回到一个开始的例子,这里一个细节,只用了一个>号来,对于自定义类型需要重载一下>号来比较,这里给一个参考的例子:
class Date
{
public:
friend bool operator >(const Date& d1, const Date& d2);
Date(int year = 2023, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
protected:
int _year;
int _month;
int _day;
};
bool operator >(const Date& d1, const Date& d2)
{
if (d1._year > d2._year)
return true;
if (d1._year == d2._year && d1._month > d2._month)
return true;
if (d1._year == d2._year && d1._month == d2._month && d1._day > d2._day)
return true;
return false;
}
int main()
{
Date d1(2023, 2, 1);
Date d2(2023, 3, 1);
cout << compare(d1, d2) << endl;
return 0;
}
如果在特殊的情况下,传入两个指针,也是不能任务的
template<class T>
int compare(const T& a,const T& b)
{
if (a > b) return 1;
if (b > a) return -1;
return 0;
}
int main()
{
int a = 10,b = 5;
cout << compare(&a, &b) << endl;
return 0;
}
输出结果
-1
7.模板的特化
想要解决指针传入比较不正确的情况,要使用模板的特化。所谓的模板特化,就是说一个或多个模板形参的实际类型或实际值是指定的。特化的形式如下:
- 必须要先有一个基础的函数模板
- 关键字template后面接一对空的尖括号<>
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型
- 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
我们使用函数特化解决一下,传入指针比较的问题:
template<class T>
int compare(const T& a,const T& b)
{
if (a > b) return 1;
if (b > a) return -1;
return 0;
}
/*
原函数模板的形参是const&,所以特化保持一致,至于是用const int*还是int*都是可以的但是类型要一致都是int*或const int*;
int compare<int*>(const int* const &a, int* const &b)是不行的
*/
template<>
int compare<int*>(int* const &a, int* const &b)
{
if (*a > *b)return 1;
if (*b > *a) return -1;
return 0;
}
int main()
{
int a = 10,b = 5;
cout << compare(&a, &b) << endl;
return 0;
}
输出结果:
1
8.类的全特化
全特化:即将模板参数列表中所有的参数都确定化。
template<class T1,class T2>
class Data
{
public:
Data() { cout << "Data<T1,T2>" << endl; }
private:
T1 _val1 = 0;
T2 _avl2 = 0;
};
template<>
class Data<int, int>
{
public:
Data() { cout << "Data<int,int>" << endl; }
private:
double val1 = 0;
double val2 = 0;
};
9.类的偏特化
偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。
template<class T1,class T2>
class Data
{
public:
Data() { cout << "Data<T1,T2>" << endl; }
private:
T1 _val1 = 0;
T2 _avl2 = 0;
};
//变化1:
template <class T1>
class Data<T1, int>
{
public:
Data() { cout << "Data<T1, int>" << endl; }
private:
T1 _d1 ;
int _d2 ;
};
//变化2:
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:
Data() { cout << "Data<T1*, T2*>" << endl; }
private:
T1 _d1;
T2 _d2;
};
int main()
{
Data<double, int> d1;
Data<int, int> d2;
Data<int*, int*> d3;
return 0;
}