- 模板的介绍
模板是懒人的福音,是提高开发效率和降低维护成本的利器。
- 相关概念
模板分为函数模板和类模板,是泛型编程的基础。
泛型编程就是编写与类无关的通用代码,使用时只需要传入实际的类型对模板进行实例化即可。
简单来说,模板就是一种万能的类型,我们使用这种类型编写出来的函数就是模板函数;使用这种类型作为类成员编写的类就是模板类。在使用模板的时候需要我们传入实际的类型对模板进行填充(这个步骤由编译器来完成),这样对于相同的逻辑我们就不必再去重载太多的函数了,若是代码有问题也只用改一份代码的逻辑,因此提高了代码的复用率的同时也减少了维护代码的耗费。
- 函数模板
-
定义
template<typename T1, typename T2,…,typename Tn>
返回值类型 函数名(参数列表) {}其中,typename可以使用class代替,效果是一样的
-
实例化
函数模板的实例化分为:隐式实例化和显式实例化隐式实例化:让编译器根据实参推演模板参数的实际类型
显式实例化:自己制定模板参数的类型下面举一个例子说明这两种实例化的区别
写了一个交换函数template<typename T> void Swap(T& left, T& right) { T tmp = left; left = right; right = tmp; }
用这个函数做一些事情
int ai = 1, bi = 2; float af = 1, bf = 2; Swap(ai, bi); Swap(af, bf); Swap(ai, bf);
前面两次交换都没有问题,但是交换ai和bf的时候出错了,因为编译器找不到符合这种情况的模板——有两个不同的模板参数的模板,为了解决这个问题,有三种解决方法
- 强制转换,使得两个参数类型相同
Swap<int>(ai, (int&)bf);
- 显示实例化
Swap<int>(ai, bf);
虽然指定函数使用int类型实例化了,但这么写编译器依然会报错,因为编译器没法将float类型转换为int引用类型。但如果函数的参数不是引用类型是可以转换的。 - 再写一个模板函数
template<typename T1, typename T2> void Swap(T1& left, T2& right) { T1 tmp = left; left = right; right = tmp; }
- 强制转换,使得两个参数类型相同
-
递归实例化
参考:template 的递归调用问题
若是这样定义一个函数模板template<int N> int fun() { if (N == 1) { return 1; } if (N == 2) { return 1; } return fun<N-1>() + fun<N - 2>(); }
编译器会报错,原因是编译器不知道要实例化多少个这样的函数,因为if是运行时判断的,而实例化是编译期完成的
于是我们只好自己写递归出口,也就是N的特例化函数注:特例化不是函数重载,而是我们自己实例化一个模板,它与显式实例化的区别在于显示实例化仍然是编译器写的,而特例化是我们自己手写的。
template<int N> int fun() { return fun<N-1>() + fun<N - 2>(); } template<> int fun<1>() { return 1; } template<> int fun<2>() { return 1; }
再写一个主函数试试
int main() { cout << fun<10>() << endl; return 0; }
好像没什么问题,编译执行后程序输出55
再修改一下参数int main() { cout << fun<4000>() << endl; return 0; }
编译报错了,错误输出和之前没写递归出口时一样
原因是,编译期编译器需要对这4000个函数一个一个进行实例化,实例化到3900多个时感觉代码段不够用了,于是直接罢工。
- 类模板
- 定义
template<class T1, class T2, …, class Tn>
class 类模板名{
//类体
}; - 实例化
类模板实例化与函数模板实例化不同,函数模板实例化时编译器可以根据输入参数的类型推导出实例化的模板,而类模板必须要显示实例化,因为编译器无法根据类的构造函数的传参推导出模板参数的类型
- 模板特例化
- 定义
在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。 - 全特化
形式- template<>
- 函数名/类名<class T1, class T2, …, class Tn>
- 偏特化
偏特化是指任何针对模版参数进一步进行条件限制设计的特化版本- 部分特化
部分特化是指将模板中的一部分参数特例化,如:
部分特化版:template<class T1,class T2> void show(T1 x, T2 y) { x = y; }
template<class T1> void show(int x, T1 y) { x = y; }
- 参数限制
这是针对模板参数类型的限制版本,如:template<class T1,class T2> void show(T1* x, T2* y) { *x = *y; }
- 部分特化
- 函数模板特例化
形式:
函数模板特例化比较少用——直接写个重载函数不就得了?template<类型表> 返回值类型 函数名<类型表>(形参表) {函数体;}
- 类模板特例化
形式:template<类型表> class 类名<类型表>{};
- 模板的应用——类型萃取
类型萃取是用来萃取类型的某些特性,从而能让函数根据类型特性的不同实现不同的功能。
有一个常用的场景就是拷贝函数,这个函数需要根据传入参数的类型进行拷贝。对于自定义类型的拷贝,比如说string,如果仅仅只是进行了内存拷贝,就会拷贝一份指针过去,进而导致二次释放的问题。于是我们首先要对参数类型进行判断,而类型萃取是最为高效快捷的一种方法。
为了书写方便先定义两个结构体类型
//是内置类型
struct TrueType {
static bool ret() {
return true;
}
};
//不是内置类型
struct FalseType {
static bool ret() {
return false;
}
};
然后写一个模板类用以判断传入的参数类型
template<class T>
struct TypeTraits {
typedef FalseType IsPODType;
};
最后根据需要书写特例化模板,这里我简单写一些内置类型
template<>
struct TypeTraits<char> {
typedef TrueType IsPODType;
};
template<>
struct TypeTraits<int> {
typedef TrueType IsPODType;
};
template<>
struct TypeTraits<short> {
typedef TrueType IsPODType;
};
template<>
struct TypeTraits<bool> {
typedef TrueType IsPODType;
};
template<>
struct TypeTraits<double> {
typedef TrueType IsPODType;
};
template<>
struct TypeTraits<float> {
typedef TrueType IsPODType;
};
template<>
struct TypeTraits<long> {
typedef TrueType IsPODType;
};
template<>
struct TypeTraits<unsigned int> {
typedef TrueType IsPODType;
};
写一个函数来测试一下:
template<class T>
void IsPodType(T& x) {
if (TypeTraits<T>::IsPODType::ret()) {
std::cout << "IsPodType" << std::endl;
}
else {
std::cout << "NotPodType" << std::endl;
}
};
这样,对IsPodType函数传入内置类型,就会打印IsPodType,否则就会打印NotPodType。
- 模板分离编译
分离编译:每个源文件单独编译生成目标文件,链接时再将所有目标文件整合成可执行文件。
由于模板是编译期进行实例化的,因此若是模板声明的头文件和模板实现的源文件分离,而使用模板的源文件只包含了模板声明的头文件,那么在编译期编译器不会对模板进行实例化,因为此时编译期在头文件中找不到模板函数的实例化,于是会发生链接错误。
- typename 和 class的区别
参考:C++中typename和class在声明模板时的区别
前面说过,如果typename或class声明模板参数,效果是一样的,那为什么C++要引进typename呢?
假设有如下场景,我们的模板参数需要传一个类类型
class A {
};
class B :public A{
};
template<class T>
void Test() {
T::A a1;
typename T::A a2;
}
试着编译一下发现编译器报错了,因为在编译器眼里T::A是一个错误的语法,它不知道这个T是个啥,它通过作用域运算符访问到的A是个啥,所以编译器懵了。而在前面声明了typename关键字之后,编译器就会将T默认为一个类,这种嵌套依赖的问题也就解决了。