泛型编程之模板详解
- 一、函数模板
- (一)函数模板的定义格式
- (二)函数模板原理
- (三)函数模板的实例化
- 1、隐式实例化:
- 2、显示实例化:
- (四)模板函数的匹配规则
- 1、优先调用非模板函数
- 2、优先调用模板函数
- 二、类模板
- 类模板定义格式和实例化
- 三、模板参数
- 四、模板的特化
- (一)概念引入
- (二)函数模板特化
- (三)类模板特化
- 1、全特化
- 2、偏特化
- (四)特化的作用
- 五、其它注意事项
- (一)访问类模板中的内嵌类型
- (二)自动类型转换
- (三)模板的分离编译
一、函数模板
我们之前在写交换函数时,一个函数只能针对一种类型的变量进行交换,如果要实现多种类型的变量呢,只能使用函数重载。但是函数重载有两个缺点:
- 代码复用率比较低,只要有新类型出现时,就需自己增加对应的函数
- 代码的可维护性比较低,一个出错可能所有的重载均出错
(一)函数模板的定义格式
模板就是给编译器一个模子,让编译器根据不同的类型利用该模子来生成代码
template<typename T>
void Swap( T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
typename
是用来定义模板参数关键字,也可以使用class
(二)函数模板原理
在编译器编译阶段,对于模板函数的使用,编译器根据传入的实参类型来推演生成对应类型的函数以供调用。
- 比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码
(三)函数模板的实例化
用不同类型的参数传给函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
1、隐式实例化:
让编译器根据实参推演模板参数的实际类型
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;
Add(a1, a2);
Add(d1, d2);//Add(a1, d1);
// 此时有两种处理方式:1. 用户自己来强制转化
//2. 使用显式实例化
Add(a, (int)d);
return 0;
}
2、显示实例化:
在函数名后的<>中指定模板参数的实际类型
int main(void)
{
int a = 10;
double b = 20.0;
// 显式实例化
Add<int>(a, b);
return 0;
}
(四)模板函数的匹配规则
1、优先调用非模板函数
一个非模板函数可以和一个同名的函数模板同时存在,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。但是该函数模板还可以被实例化为这个非模板函数,显示实例化后依旧可以调用这个模板函数。
// 专门处理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版本
}
2、优先调用模板函数
模板可以产生一个具有更好匹配的函数, 那么将选择模板
// 专门处理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函数
}
二、类模板
类模板定义格式和实例化
类模板在实例化时,我们在传入变量的类型时就确定了其中的类型,成员属性已经确定了,但是成员函数是按需实例化。
template<typename T>
class Stack
{
public:Stack(size_t capacity = 4){_array = new T[capacity];_capacity = capacity;_size = 0;}void Push(const T& data){_array[_size] = data;++_size;}private:T* _array;size_t _capacity;size_t _size;
};int main()
{
Stack<int> st1; // int
Stack<double> st2; // double
return 0;
}
三、模板参数
模板参数分类类型形参与非类型形参。
- 类型形参即:出现在模板参数列表中,跟在
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;
};
四、模板的特化
(一)概念引入
模板在使用过程中,在一些特殊场景时可能会发生一些错误,比如在下面的代码中,我们传入两个指针给比较函数,发现函数比较的不在是指针的内容,而是地址
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;
}
而解决这个问题的方式就是进行模板的特化,在编译时,编译器给我们特化的版本不符合我们的预期,我们就自己来进行模板的特化。
(二)函数模板特化
我们可以将上面的模板特化出专门针对指针的版本,那么编译器不会再去特化。
template<class T>
bool Less(T left, T right)
{return left < right;
}
// 对Less函数模板进行特化
template<>
bool Less<Date*>(Date* left, Date* right)
{return *left < *right;
}
注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。该种实现简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化时特别给出,因此函数模板不建议特化。
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;
};
1、全特化
直接指明类模板的所有参数类型,如果匹配的话,优先走特化
template<>
class Data<int, char>
{
public:
Data() {cout<<"Data<int, char>" <<endl;}
private:
int _d1;
char _d2;
};
2、偏特化
指明类模板的部分参数类型,或者是对参数的进一步限制(例如限制传入的值为指针 / 引用)。
模板有现成吃现成,所以或优先匹配下面偏特化后第二个值为 int 的版本
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;
};
(四)特化的作用
之前我们在对日期类进行排序时,发现如果我们传入的是地址,那么结果就是对指针排序。前面我们通过另外写一个仿函数解决了这个问题,这里,我们可以使用偏特化对参数类型进行限定,也能解决这个问题。
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);sort(v2.begin(), v2.end(), Less<Date*>());return 0;
}
template<>
struct Less<Date*>
{bool operator()(Date* x, Date* y) const{return *x < *y;}
};
五、其它注意事项
(一)访问类模板中的内嵌类型
因为是类模板,它里面的成员按需实例化。而类名::
的方式访问时因此编译器认为他有可能是一个成员变量,也有可能是一个内嵌类型(内部类 / typedef
),为了区别这两种情况,我们
template<class Container>
void Print(const Container& c)
{// const_iterator可能是静态成员变量,也可能内嵌类型(内部类,typedef)//typename Container::const_iterator it = c.begin();auto it = c.begin();while (it != c.end()){cout << *it << " ";}cout << endl;
}
(二)自动类型转换
模板函数不允许自动类型转换,但普通函数可以进行自动类型转换。
(三)模板的分离编译
- 将声明和定义放到一个文件
xxx.hpp
或者xxx.h
里面。 - 模板定义的位置显式实例化,用一个实例化一个,有点累赘和不显示