目录
非类型模板参数
在了解模板进阶之前,先看一个特性:例如我们要搞一个可以变化容量大小的栈,在C语言中是这样定义的:
#define N 100
// 静态的栈
template<class T>
class Stack
{
private:
int _a[N];
int _top;
};
int main()
{
Stack<int> st1; // 10
Stack<int> st2; // 100
return 0;
}
通过修改宏的值来修改容量大小,在C++中我们不这样操作, 我们在模板上直接增加一个非类型模板参数。例如以下代码所示:
// 静态的栈
template<class T, size_t N = 10>
class Stack
{
public:
void func()
{
// N是常量
N++;
}
private:
int _a[N];
int _top;
};
int main()
{
Stack<int> st1; // 10
Stack<int, 100> st2; // 100
Stack<int, 1000> st3; // 1000
// 按需实例化
//st1.func();
return 0;
}
以上的size_t N就是一个非类型模板参数(C++20之前,只允许整形做非类型模板参数,而C++20之后,可以支持double等其他内置类型)
为什么这里N++不调用func就不会报错呢?原因如下:编译器只会对模板大体有一个检验,它不会去查看类内容是否正确,只有在main函数中将它实例化后,才会发生编译错误。
类模板没实例化时,不去里面查细节东西,无法确认时类型还是静态变量,我们再浅看一下类类型形参,类类型形参就是跟在typename后面的参数:
void PrintVector(const vector<int>& v)
{
vector<int>::const_iterator it = v.begin();
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
template<class T>
void PrintVector(const vector<T>& v)
{
// 加typename明确告诉是类型
//typename vector<T>::const_iterator it = v.begin();
auto it = v.begin();
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
int main()
{
vector<int> v1 = { 1,2,3,4,5,6,7 };
vector<double> v2 { 1.1,2.2,3.3,4.4,5.5,6.6,7.7 };
PrintVector(v1); //调用第一个
PrintVector(v2); //调用第二个
return 0;
}
简单介绍一下模板参数分为:类类型形参与非类型形参。
类类型形参:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
非类型形参:就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。另外注意以下两点:
- 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
- 非类型的模板参数必须在编译期就能确认结果。
模板的特化
概念
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板:
// 函数模板 -- 参数匹配
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;
}
可以看到,Less绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果。上述示例中,p1指 向的d1显然小于p2指向的d2对象,但是Less内部并没有比较p1和p2指向的对象内容,而比较的是p1和p2指针的地址,这就无法达到预期而错误。
此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特 化中分为函数模板特化与类模板特化。
函数模板特化
函数模板的特化步骤:
- 必须要先有一个基础的函数模板
- 关键字template后面接一对空的尖括号<>
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型
- 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
template<class T>
bool Less(T left, T right)
{
return left < right;
}
//特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
return *left < *right;
}
int main()
{
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;
}
如果是const修饰的变量就要注意const放的位置,如下代码所示:
template<class T>
bool Less(const T& left, const T& right)
{
return left < right;
}
//特化
template<>
bool Less<Date*>(Date* const & left, Date* const & right)
{
return *left < *right;
}
在模板中const修饰的是变量,而不是指向的内容,所以特化后的模板也要修饰相同内容,如果放在Date*之前则表示指向的内容不可修改,而本身可修改,与原模版权限相违背。
注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。比如日期的比较,模板不如函数可读性高,能用函数尽量用函数。
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<int, char>
{
public:
Data() { cout << "Data<int, char>- 全特化" << endl; }
};
可以看到T1,T2都被给了明确的类型,这就是全特化。
偏特化
偏特化:也叫半特化,任何针对模版参数进一步进行条件限制设计的特化版本。
偏特化有以下两种表现方式:
1、部分特化:将模板参数类表中的一部分参数特化。
// 偏特化/半特化
template <class T1>
class Data<T1, int>
{
public:
Data() { cout << "Data<T1, int>-偏特化" << endl; }
private:
T1 _d1;
int _d2;
};
根据上述概念,我们用一些实例进行测试,进一步区分三者区别:
int main()
{
Data<int, int> d1; //Data<T1, int>-偏特化
Data<int, char> d2;//Data<int, char>-全特化
Data<int, double> d3;//Data<T1, T2>-原模版
return 0;
}
d1由于只有第二个参数符合偏特化,优先调用,d2两个参数类型都符合全特化,先调用准备好的,d3两个参数都与全特化和偏特化不同,只能调用原模板。
2、参数更进一步的限制:偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
// 限定模版的类型
// 两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:
Data() {
cout << typeid(T1).name() << endl;
cout << typeid(T2).name() << endl;
cout << "Data<T1*, T2*>" << endl << endl;
}
};
//两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
Data()
{
cout << typeid(T1).name() << endl;
cout << typeid(T2).name() << endl;
cout << "Data<T1&, T2&>" << endl << endl;
}
private:
};
template <typename T1, typename T2>
class Data <T1&, T2*>
{
public:
Data()
{
cout << typeid(T1).name() << endl;
cout << typeid(T2).name() << endl;
cout << "Data<T1&, T2*>" << endl << endl;
}
private:
};
我们依旧用几个例子来看偏特化的区别:
int main()
{
//int
//double
//Data<T1*, T2*>-偏特化
Data<int*, double*> d4;
//int
//int*
//Data<T1*, T2*>-偏特化
Data<int*, int**> d5;
//int
//int
//Data<T1&, T2&>
Data<int&, int&> d6;
//int
//int
//Data<T1&, T2*>
Data<int&, int*> d7;
return 0;
}
虽然模板上为引用或指针,但是模板类型依旧为内置类型,原因是为了处理多种情况而不只是应对指针或者引用。
例如我们要排序一个优先级队列,push的是每个元素的地址,单纯比较无法得出正确答案,因为比较的是元素的地址。这时候就要偏特化出一个指针类型,比较的是指针指向的内容。
模板分离编译
什么是分离编译?一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链 接起来形成单一的可执行文件的过程称为分离编译模式。简单理解就是.h和.cpp两个文件分别存放定义和声明。
假如有以下场景,模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:
// Func.h
#pragma once
#include<iostream>
using namespace std;
template<class T>
T Add(const T& left, const T& right);
void func();
// Func.cpp
#include"Func.h"
template<class T>
T Add(const T& left, const T& right)
{
cout << "T Add(const T& left, const T& right)" << endl;
return left + right;
}
void func()
{
cout << "void func()" << endl;
}
以上声明和定义分离,如果我们要调用add函数模板能够成功吗?
#include"Func.h"
int main()
{
// 函数模版链接找不到
Add(1, 2); // call Z3Addii()
Add(1.0, 2.0); // call Z3Adddd()
Add('x', 'y'); // call Z3Addcc()
// 普通函数可以链接找到
func();
return 0;
}
可以看到我们无法使用函数模板,原因有以下几点:
由于模板没有被实例化,add去.h文件中查找,虽然有声明但是没有定义,不知道如何实例化,而.cpp中有定义,但是main函数不会进里面找,不知道实例化成什么,但是func函数由于不要实例化,直接生成地址放在符号表里,需要调用时直接使用即可。
当然应对这种情况也有解决办法,最佳解决方案:不要分离到两个文件,写到一个文件(原理:调用的地方,就有定义,就直接实例化)比如以上的add函数模板,我们把声明和定义都放在.h文件中。
template<class T>
T Add(const T& left, const T& right)
{
cout << "T Add(const T& left, const T& right)" << endl;
return left + right;
}
如果是类模板,长的成员函数,声明和定义分离,写到当前文件夹外面,短的直接定义在类里面(声明和定义依旧保证在同一个文件里.h)
另外一种方法是将模板定义的位置显式实例化。这种方法不实用,不推荐使用。
// Func.cpp
#include"Func.h"
template<class T>
T Add(const T& left, const T& right)
{
cout << "T Add(const T& left, const T& right)" << endl;
return left + right;
}
// 显示实例化,这种解决方式很被动,需要不断添加显示实例化
template
int Add(const int& left, const int& right);
template
double Add(const double& left, const double& right);
void func()
{
cout << "void func()" << endl;
}
模板总结
优点 缺点 模板复用了代码,节省资源,更快的迭代开发,STL因此而产生 模板会导致代码膨胀问题,也会导致编译时间变长 增强了代码的灵活性 出现模板编译错误时,错误信息非常凌乱,不易定位错误