总言
主要介绍模板相关内容:非类型模板参数、类模板特化、模板的分离编译。
文章目录
- 总言
- 1、非类型模板参数
- 1.1、主要介绍
- 1.2、std::array 简要说明
- 2、模板的特化
- 2.1、基本介绍
- 2.2、函数模板特化
- 2.3、类模板特化
- 2.3.1、基本说明
- 2.3.2、用途举例
- 2.3.3、分类:全特化、偏特化
- 3、模板的分离编译
1、非类型模板参数
1.1、主要介绍
1)、问题引入
在之前,我们已经对模板有一定了解:
#define N 5
template<class T>
class Array
{
private:
T _a[N];
};
int main()
{
Array<int> a1;
Array<double> a2;
return 0;
}
根据上述情况,我们能使用模板定义出两个类型不同的类,但是,假如我们需要a1大小为10,a2大小为8,该如何定义呢?
这是我们就需要非类型模板参数。
2)、非类型模板参数介绍
模板参数分类类型形参与非类型形参。
类型形参:出现在模板参数列表中,跟在class
或者typename
之后的参数类型名称。
非类型形参:用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
举例如下:此处的size_t N
即非类型模板参数。
template<class T, size_t N>
class Array
{
private:
T _a[N];
};
int main()
{
Array<int,10> a1;
Array<double,8> a2;
return 0;
}
注意事项:
1、非类型模板参数只能是常量,因此其限制了变长数组的使用。
2、非类型模板参数也可以使用缺省值。
template<class T, size_t N=5>
struct Array
{
T _a[N];
};
int main()
{
Array<int> a1;
Array<double,8> a2;
return 0;
}
3、非类型模板参数限制为整型(包含char),浮点数、类对象以及字符串是不允许作为非类型模板参数的。
4、非类型的模板参数必须在编译期就能确认结果。
1.2、std::array 简要说明
事实上,库里也有一个array:相关链接
其相关使用和数组一致,细微之处在于多了迭代器的各接口。那么,有一个问题:既然有了数组,为什么还要单独创建一个array的类?
Array<int> a1;
int arr[5];
实际上,主要的区别在于:对越界的检查。
Array<int> a1;
:属于函数调用,只要越界,就能检查到。
int arr[5];
:属于指针解引用 ,其越界检查属于设岗抽查,且只针对越界写,越界读不检查。
2、模板的特化
2.1、基本介绍
1)、问题引入
如下,我们写一个Less
函数模板,用于比较不同类型大小:
template<class T>
bool Less(T left, T right)
{
return left < right;
}
int main()
{
// 可以比较,结果正确
cout <<"Less(1, 2):" << Less(1, 2) << endl;
// 可以比较,结果正确
Date d1(2023, 4, 29);
Date d2(2023, 6, 19);
cout << "Less(d1, d2):" << Less(d1, d2) << endl;
// 可以比较,结果错误
Date* p1 = &d1;
Date* p2 = &d2;
cout << "Less(p1, p2):" << Less(p1, p2) << endl;
return 0;
}
可以发现:Less
适用于绝对多数场景,但是在特殊场景下会得到错误的结果。如上述Less(p1, p2)
,对此分析,这是因为p1
、p2
为指针类型,指向的是Date对象的地址,我们期望Less
函数内部比较的是p1
和p2
指向的对象内容(d1
,d2
),但实际比较的是p1
和p2
指针的地址。
针对上述情况,就需要用到模板特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。
struct Date
{
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{}
bool operator>(const Date& d) const
{
if ((_year > d._year)
|| (_year == d._year && _month > d._month)
|| (_year == d._year && _month == d._month && _day > d._day))
{
return true;
}
else
{
return false;
}
}
bool operator<(const Date& d) const
{
if ((_year < d._year)
|| (_year == d._year && _month < d._month)
|| (_year == d._year && _month == d._month && _day < d._day))
{
return true;
}
else
{
return false;
}
}
int _year;
int _month;
int _day;
};
模板特化中分为函数模板特化与类模板特化。
2.2、函数模板特化
1)、使用说明
template<class T>
bool Less(T left, T right)
{
return left < right;
}
仍旧是上述例子,我们对Less函数模板进行特化处理:
template<class T>
bool Less(T left, T right)
{
return left < right;
}
template<>
bool Less<Date*>(Date* left, Date* right)
{
return *left < *right;
}
结果如下:
函数模板的特化步骤:
1、必须要先有一个基础的函数模板
2、关键字template
后面接一对空的尖括号<>
3、函数名后跟一对尖括号,尖括号中指定需要特化的类型
4.、函数形参表:必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
当然我们也可直接使用非模板函数:(这里有一个模板参数的匹配原则,相关内容见模板初阶章节)
bool Less(Date* left, Date* right)
{
return *left < *right;
}
2.3、类模板特化
2.3.1、基本说明
除了函数模板特化,类模板也可以根据需求进行特化处理,以下为相关演示:
namespace myless
{
//类模板
template<class T>
struct less
{
bool operator()(const T& val1, const T& val2)
{
return val1 < val2;
}
};
}
void test06()
{
Date d1(2023, 4, 29);
Date d2(2023, 6, 19);
myless::less<Date> lessFun1;
cout << lessFun1(d1, d2) << endl;
Date* p1 = &d1;
Date* p2 = &d2;
myless::less<Date*> lessFun2;
cout << lessFun2(p1, p2) << endl;
}
同样定义一个类模板,当我们传入参数不同时,存在场景使用错误,因此类模板中也需要特化处理。
namespace myless
{
//类模板
template<class T>
struct less
{
bool operator()(const T& val1, const T& val2)
{
return val1 < val2;
}
};
template<>//注意模板特化需要处理的地方
struct less<Date*>//
{
bool operator()(Date* val1, Date* val2)//
{
return *val1 < *val2;
}
};
}
2.3.2、用途举例
我们以优先级队列来举例演示:
分别用date类构建两个优先级队列,根据之前所学,优先级队列实则是以堆排序数据的,因此我们将相同的date数据传入优先级队列中,再分别拿出打印:
#include<queue>
void test07()
{
std::priority_queue<Date,vector<Date>,myless::less<Date>> pq1;
std::priority_queue<Date*, vector<Date*>, myless::less<Date*>> pq2;
pq1.push(Date(2023, 4, 29));pq1.push(Date(2023, 6, 19));pq1.push(Date(2023, 3, 07));pq1.push(Date(2023, 4, 20));
pq1.push(Date(2023, 9, 18));pq1.push(Date(2023, 7, 11));pq1.push(Date(2023, 8, 24));
while (!pq1.empty())
{
cout << pq1.top();
pq1.pop();
}
cout << "________________________________________________" << endl;
pq2.push(new Date(2023, 4, 29)); pq2.push(new Date(2023, 6, 19)); pq2.push(new Date(2023, 3, 07)); pq2.push(new Date(2023, 4, 20));
pq2.push(new Date(2023, 9, 18)); pq2.push(new Date(2023, 7, 11)); pq2.push(new Date(2023, 8, 24));
while (!pq2.empty())
{
cout << *(pq2.top());
pq2.pop();
}
}
结果如下:可看到,在没有对模板进行特化处理时,以Date*
构建出的优先级队列pq2
,其结果非按照大堆排序,实则排序的是new出来的地址空间。
namespace myless
{
//类模板
template<class T>
struct less
{
bool operator()(const T& val1, const T& val2)
{
return val1 < val2;
}
};
//template<>
//struct less<Date*>
//{
// bool operator()(Date* val1, Date* val2)
// {
// return *val1 < *val2;
// }
//};
}
2.3.3、分类:全特化、偏特化
1)、全特化、偏特化举例
以下述类模板举例:
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;
}
};
偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。
偏特化演示一:部分特化,将模板参数类表中的一部分参数特化。
template<class T1>//注意偏特化中,这里模板参数需要给出
class Data<T1, int>
{
public:
Data() { cout << "Data<T1, int>" << endl; }
};
偏特化演示二:偏特化不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
template<class T1,class T2>
class Data<T1*, T2*>//这也是偏特化的一种,这里限制了参数必须是指针类型
{
public:
Data() { cout << "Data<T1*, T2*>" << endl; }
};
相关结果:
template<class T1, class T2>
class Data<T1&, T2&>//模板参数还可以是引用
{
public:
Data() { cout << "Data<T1*, T2*>" << endl; }
};
template<class T1, class T2>
class Data<T1*, T2&>//也可以是二者结合
{
public:
Data() { cout << "Data<T1*, T2&>" << endl; }
};
template<class T1, class T2>
class Data<T1, T2&>//也可以是二者结合
{
public:
Data() { cout << "Data<T1, T2&>" << endl; }
};
3、模板的分离编译
1)、问题说明
我们以vector来举例说明:
//vector.h文件
namespace myvector
{
template<class T>
class vector
{
public:
//这里为了观察省去一部分成员函数
//……
//尾删数据
void pop_back()
{
assert(_finish > _start);
_finish--;
}
iterator insert(iterator pos, const T& val);
void push_back(const T& val);
private:
iterator _start;
iterator _finish;
iterator _end_of_storage;
};
}
此处以insert
、push_back
来举例说明,我们将其在类中声明,在类外定义:
注意这里的各种写法:
1、vector<T>::push_back
、vector<T>::insert
类外使用,要注意指定类域,如果不加命名空间,则为myvector::vector<T>::push_back
2、typename vector<T>::iterator
,加上该关键字是为了区别后面iterator
是类中的一个类型,而非静态类成员,因为静态类成员也可以使用类域直接访问。
//vector.cpp文件
namespace myvector
{
template<class T>
typename vector<T>::iterator typename vector<T>::insert(typename vector<T>::iterator pos, const T& val)//vector<T>::iterator、vector<T>::insert 类外使用,要注意指定类域
{
assert(pos >= _start && pos <= _finish);
if (_finish == _end_of_storage)
{
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : capacity() * 2);
pos = _start + len;
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = val;
++_finish;
return pos;
}
template<class T>
void vector<T>::push_back(const T& val)
{
//检查
if (_finish == _end_of_storage)
{
reserve(capacity() == 0 ? 4 : capacity() * 2);
}
//插入
*_finish = val;
++_finish;
}
}
以下为测试代码:
//test.cpp
#include"vector.h"
void test09()
{
myvector::vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
for (size_t i = 0; i < v1.size(); ++i)
{
cout << v1[i] << " ";
}
}
我们运行上述代码:可发现结果报错,且是链接错误。但当我们没使用insert、push_back声明和定义分离的这两个函数时,成功运行。
原因解释:
1、test.cpp
文件中包含了头文件vector.h
,头文件在编译阶段会展开,而故头文件中定义的函数operator[]、size等
,后续vector<int> v1
实例化时,这些成员函数都跟随实例化,也就有了具体定义,那么编译阶段能够直接确定地址。
2、insert、push_back
声明和定义分离,vector.h
中只有二者声明,而test.cpp
中我们使用这两个函数,那么即使头文件被展开,在编译阶段我们没有得到二者的确切地址,故只能在链接阶段所有.obj文件汇总后去寻找相关地址。
3、但我们得到的结果是报错,说明链接阶段没有找到insert、push_back
的地址,这是因为声明定义分离后,其中模板参数T
无法确定,即二者没有实例化,相应地址也就没有进入符号表,故链接出错。
2)、解决方案
关于模板声明定义分离解决方案:
1、将声明和定义放到同一个文件里,比如 “xxx.hpp
” 或者xxx.h
,在该基础上,类里声明,类外实现函数具体方法。
2、模板定义的位置显式实例化,如下:这种写法存在的一个缺陷是把类型写死了。
//vector.cpp文件
namespace myvector
{
template<class T>
typename vector<T>::iterator typename vector<T>::insert(typename vector<T>::iterator pos, const T& val)//vector<T>::iterator、vector<T>::insert 类外使用,要注意指定类域
{
assert(pos >= _start && pos <= _finish);
if (_finish == _end_of_storage)
{
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : capacity() * 2);
pos = _start + len;
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = val;
++_finish;
return pos;
}
template<class T>
void vector<T>::push_back(const T& val)
{
//检查
if (_finish == _end_of_storage)
{
reserve(capacity() == 0 ? 4 : capacity() * 2);
}
//插入
*_finish = val;
++_finish;
}
//针对整个类进行显示实例化的的方法:
template
vector<int>;
template
vector<double>;
}
3)、模板总结
优点:
1、模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
2、增强了代码的灵活性
缺陷:
1、模板会导致代码膨胀问题,也会导致编译时间变长
2、出现模板编译错误时,错误信息非常凌乱,不易定位错误