模板
一、泛型编程
泛型编程: 编写通用的、具有普适性的代码,以处理各种不同类型的数据。编写与类型无关的通用代码,是代码复用的一种手段。模板就是泛型编程的基础
在没有学习模板之前,通过之前学习的知识,我们知道了函数重载。可以通过函数重载实现不同类型的相同功能的同名函数。函数重载
函数重载的缺点:
- 代码复用率低,只要有新的类型出现,就需要用户增加对应的函数。
- 可维护性低,一个出错可能所有的重载都出错。
eg:函数重载
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;
}
这时就需要一个模子,告诉编译器根据不同的类型生成需要的代码。所以出现了模板
二、介绍模板
模板分为:函数模板和类模板
1. 函数模板
①概念
函数模板代表了一个函数家族,该函数模板和类型无关,在使用时根据实参类型产生函数的特定类型版本。模板参数化
函数模板的语法:
template<typename T1, typename T2,...., typename Tn>
返回值类型 函数名(参数列表) {}
注: typename是用来定义模板参数的关键字,typename也可以换成class。但是struct不能替换typename,在类名中struct和class可以替换使用,这里不行
eg:
template<typename T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
int main()
{
int a1 = 10;
int b1 = 1;
Swap(a1, b1);
cout << "a1=" << a1 << " b1=" << b1 << endl;
double a2 = 1.1;
double b2 = 10.1;
Swap(a2, b2);
cout << "a2=" << a2 << " b2=" << b2 << endl;
//...
return 0;
}
//运行结果:
// a1=1 b1=10
// a2=10.1 b2=1.1
②原理
函数模板是一个蓝图,他本身不是函数,是编译器使用方式产生特定具体类型函数的模具。所以模板就是将本来我们做的重复的事交给编译器。
编译器在编译阶段,对于使用的函数模板,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。
图解:
③函数模板实列化
函数模板实列化:用不同类型的参数使用函数模板。模板参数实例化分为:隐式实例化和显式实例化
- 隐式实例化
eg:让编译器根据实参推演模板参数的实际类型。
template<typename T>
//这const要加,因为实参被强制类型转化
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
Add(a1, a2);
Add(d1, d2);
//因为模板参数列表就一个T,编译器无法确定T是int类型还是double类型
//在模板中,编译器不会进行类型转换操作,因为转换出现问题,就是编译器的原因
//Add(a1, d1); //err
//两种处理方式:1.用户自己强转 2.使用显示实列化(后面讲)
Add(a1, (int)d1); //强转d1生成临时变量,不影响d1本身
Add((double)a1, d1);
return 0;
}
- 显示实例化: 在函数名后的<>中指定模板参数的实际类型。
eg1:
template<typename T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a = 10;
double b = 20.1;
//显示实列化
Add<int>(a, b); // b 在这里就会隐式类型转换
return 0;
}
类型不匹配编译器会尝试进行隐式类型转换,无法转换成功编译器会报错。
eg2:有写函数无法自动推,只能显示实列化。
template<class T>
T* Alloc(int n)
{
return new T[n];
}
int main()
{
//只能显示实列化
double* p1 = Alloc<double>(10);
//double* p1 = Alloc(10); //err
return 0;
}
④模板参数匹配原则
- 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
eg1:
int Add(int left, int right)
{
return left + right;
}
template<class T>
T Add(T left, T right)
{
return left + right;
}
int main()
{
Add(1, 2); //与非模板函数匹配,编译器不需要特化
Add<int>(1, 2); //调用编译器特化的Add版本
return 0;
}
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板。
eg2:
int Add(int left, int right)
{
return left + right;
}
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
return left + right;
}
int main()
{
Add(1, 2); //与非模板函数类型完全匹配,不需要函数模板实列化
Add(1, 2.0); //模板函数可以生成更加匹配的版本
return 0;
}
- 在 C++ 中,模板函数不会自动进行类型转换,而普通函数会。
eg3:
template <typename T>
void templateFunction(T value) {
std::cout << "Template function: " << value << std::endl;
}
void normalFunction(int value) {
std::cout << "Normal function: " << value << std::endl;
}
int main() {
int intValue = 10;
double doubleValue = 3.14;
templateFunction(intValue); // 不会进行类型转换,输出:Template function: 10
templateFunction(doubleValue); // 不会进行类型转换,输出:Template function: 3.14
normalFunction(intValue); // 进行了隐式类型转换,输出:Normal function: 10
normalFunction(doubleValue); // 进行了隐式类型转换,输出:Normal function: 3
return 0;
}
在上述代码中,
- templateFunction是一个模板函数,它可以接受任意类型的参数。无论传入的参数是int还是double,都不会进行类型转换,而是直接使用传入的参数类型。
- 而normalFunction是一个普通函数,它的参数类型是int。当我们传入一个double类型的参数时,编译器会自动进行隐式类型转换,将doubleValue的值转换成int类型,然后调用该函数。
因此,模板函数不允许自动类型转换,而普通函数可以进行自动类型转换。
2. 类模板
①类模板的定义格式
语法:
template<class T1, class T2, ...,class Tn>
class 类模板名
{
//类内成员定义
};
eg: 类模板中的函数放在类外进行定义时,就需要加模板参数列表
template<class T>
class vector //把这个就当作一个数组的模板类
{
public:
Vector(size_t capacity = 10)
: _pData(new T[capacity])
, _size(0)
, _capacity(capacity)
{}
//类中声明,类外定义
~vector();
private:
T* _pData;
size_t _size;
size_t _capacity;
};
//加模板参数列表
template<class T> //模板参数列表
Vector<T>::~Vector()
{
if (_a)
{
delete[] _a;
}
_size = _capacity = 0;
}
②类模板的实列化
类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类
eg:
// Vector类名,Vector<int>才是类型
Vector<int> s1;
Vector<double> s2;
小结
- 模板最重要的一点就是类型无关,提高代码复用率
- 模板运行时不检查数据类型,也不保证类型安全,相当于类型的宏替换
- 只要支持模板语法,模板的代码就是可移植的。
- 模板的实参,在参数类型不同时,有时需要展示指定的类型参数
- 类模板是一个类家族,模板类是通过模板实列化的具体类。
- 类模板的虚拟类型是指类模板在使用时的占位类型
- 类模板的成员函数,全是模板函数。
三、class和typename区别
template<class Container>
void Print(const Container& v)
{
Container::const_iterator it = v.begin();
//...
}
分析代码:
四、非类型模板参数
模板参数:类型形参和非类型形参
- 类型形参:出现在模板参数列表中,跟在class或者typename的参数类型
- 非类型形参:常量作为类(函数)模板的一个参数,在模板中可将该参数当常量使用
eg:
namespace kpl //命名空间,有疑惑的可以看一下我前面写的博客
{
//定义一个模板类型的静态数组
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;
};
}
int main()
{
//想要静态存储10个,或者100个数据
//array<int> a1; 存10个数据
//array<int> a2; 存100个数据
//为了解决上述问题,出现非类型模板参数
kpl::array<int, 10> a1; //kpl::array -> 使用命名空间的方式
kpl::array<int, 100> a2;
return 0;
}
非类型模板参数的限制:(了解)
- 浮点数、类对象以及字符串不允许作为非类型模板参数
- 非类型的模板参数必须在编译期间就能确认结果 。
五、类模板的特化
类模板的特化分为:函数模板特化和类模板特化
1. 概念
对于一些特殊类型,使用模板可能会得到一些错误的结果,所以就需要特殊处理
eg1:例子中借用自定义类型,日期类Date类的实现
template<class T>
bool Less(T left, T right)
{
return left < right;
}
int main()
{
cout << Less(1, 2) << endl; //结果正确
Date d1(2023, 8, 10);
Date d2(2023, 8, 18);
cout << Less(d1, d2) << endl; //结果正确
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl; //结果错误
return 0;
}
代码分析:
- 在上述这种场景下得到的结果错误。我们使用日期类指针比较,发现结果不是日期的比较而是指针的比较。
- 为了解决上述问题就需要对模板进行特化。即在原模板类的基础上,针对特殊类型进行特殊化的实现方式
2. 函数模板特化
基本不使用函数模板特化,都是重新实现函数个函数即可
要求:
- 必须要先有一个基础的函数模板
- 关键字template后跟<>
- 函数名后跟<>,尖括号中指定需要特化的类型
- 函数形参表:必须和模板函数的基础参数类型完全相同。
eg1:例子中借用自定义类型日期类,Date类的实现
//函数模板
template<class T>
bool Less(T left, T right)
{
return left < right;
}
//对Less函数模板进行特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
return *left < *right;
}
int main()
{
cout << Less(1, 2) << endl;
Date d1(2023, 8, 10);
Date d2(2023, 8, 18);
cout << Less(d1, d2) << endl;
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl; //调用特化版本,而不是走模板生成
}
注:一般如果函数模板遇到不能处理或处理有误的情况,为了实现简单,通常都是将该函数直接实现。(原因:实现简单,不易出错,后面的一些模板参数可能很复杂)
eg2:
bool Less(Date* left, Date* right)
{
return *left < *right;
}
3. 类模板特化
①全特化
全特化即将模板参数列表中所有的参数都确定化
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;
};
int main()
{
Data<int, int> d1;
Data<int, char> d2;
return 0;
}
//output:
//Data<T1, T2>
//Data<int, char>
②偏特化
偏特化即任何针对模板参数进一步进行条件限制的特化版本
eg: 先给一个基础模板
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 _d1;
T2 _d2;
};
偏特化的两种表现方式:
- 部分特化
将模板参数表一部分特化
//将第二个参数转成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;
};
偏特化代码测试:
int main()
{
Data<double, int> d1; //调用特化的int版本
Data<int, double> d2; //调用基础版本
Data<int*, int*> d3; //调用特化的指针版本
Data<int&, int&> d4(1, 2); //调用特化的引用版本
return 0;
}
//output:
//Data<T1, int>
//Data<T1, T2>
//Data<T1*, T2*>
//Data<T1&, T2&>
六、模板的分离编译
1. 概念
分离编译模式:一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程
eg:
头文件声明: a.h
template<class T>
T Add(const T& left, const T& right);
源文件完成定义: a.cpp
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
在main函数调用Add: main.cpp
#include "a.h"
int main()
{
Add(1, 2);
Add(1.0, 2.0);
return 0;
}
代码分析:
在模板的分离编译中,调用的地方只能看到声明而无法看到定义。模板的声明通常会被包含在头文件中,模板的定义实现通常会被放在源文件中。在编译器编译调用模板的文件时,只能看到声明部分,无法看到实现部分,因此无法进行实例化。要实例化模板,需要在模板的定义所在的源文件中显式实例化模板的具体类型。
2. 解决方法
- 在模板定义的位置显示实例化(不推荐)—— eg1
eg1:比较麻烦,需要什么类型的就要实例化一份
//这样上面的代码就可以正常运行了
template
class Add<int>;
template
class Add<double>;
- 将声明和定义放在一个文件中。例如同时放入xxx.hpp(.hpp的后缀表示声明和定义放在了一起)里面或者xxx.h也可以(推荐) —— eg2
eg2:
namespace kpl
{
template<class T, class Container = std::deque<T>>
class stack
{
public:
//类中声明,类外定义,但是都在同一个文件
void push(const T& x);
void pop();
private:
Container _con;
};
//同一个文件中,在类外定义
template<class T, class Container>
void stack<T, Container>::push(const T& x)
{
_con.push_back(x);
}
template<class T, class Container>
void stack<T, Container>::pop()
{
_con.pop_back();
}
}
总结
- 优点
- 模板复用了代码,节约资源,更快的迭代开发
- 增强了代码的灵活行
- 缺点
- 导致代码膨胀的问题,也导致编译时间长
- 模板编译出现错误,错误信息长且乱,不便解决