目录
1. 非类型模板参数
模板参数分类为:类型形参与非类型形参,如下:
- 类型形参:出现在模板参数列表中,跟在class或者typename之类的参数类型名称;
- 非类型形参:就是用一个常量作为类 (函数) 模板的一个参数,在类 (函数) 模板中可将该参数当成常量来使用。
#define N 100
template<class T>
class array
{
private:
T _a[N];
};
void Test2(void)
{
Xq::array<int> a1;
Xq::array<char> a2;
}
上面的a1和a2这两个数组是根据模板参数实例化的得到的两个数组, 但如果我想让a1有50个空间,a2有100个空间,那么如何做呢?
可能我们第一反应想到的就是重写一个类,一个类里面固定为100,一个类里面固定为50,但这就不能达到泛型了;
因此在这里就有一个新东西:非类型模板参数,如下:
//这里的 N 是非类型模板参数 --- 是一个常数
//同样这个N也可以给缺省值
template<class T,size_t N = 10>
class array
{
private:
T _a[N];
};
void Test3(void)
{
Xq::array<int,50> a1;
Xq::array<char,100> a2;
}
注意:
- 浮点数、类对象以及字符串是不允许作为非类型模板参数的 (一般只能是整形);
- 非类型的模板参数必须在编译期间就能确认结果。
template<class T,double N = 10.1>
class array
{
private:
T _a[N];
};
此时编译就会报错,现象如下:
同理,像字符串、float、自定义类型等都不可以作为非类型模板参数。
1.1. std::array和C数组的区别
我们查看STL标准库,这里面有个容器叫array:
// std::array
// Arrays are fixed-size sequence containers //即一个静态数组
template < class T, size_t N > class array;
那么这个array和C语言上面的数组有什么区别呢?
void Test1(void)
{
//C++期望你用的
std::array<int, 10> a1;
//C(C++为了兼容C)
int a2[10];
}
二者真正有很大差别的地方是对越界的检,具体如下:
void Test1(void)
{
int a[10];
std::cout << a[10] << std::endl;
std::cout << a[20] << std::endl;
}
结果如下:
可以看到,对于C而言,对于读的越界,C数组检查不出来
那么 std::array 呢? 测试如下:
void Test2(void)
{
std::array<int, 10> a1;
int a2[10];
cout << a1[10] << endl; // std::array 会对越界进行强制(读写)检查
}
现象如下:
C数组和 std::array 都会对写操作进行检查,但是C数组是一种抽查检查,只会检查与边界的相邻的一些位置,而std::array只要你越界了,都会检查出来(不管你是r/w)。
如下,C数组对越界写操作可以检查出来:
但对写的检查也是一种抽查,只会检查与边界相邻的一些位置,有些位置的越界写,C数组依旧检查不出来,如下:
而对于std::array来说,只要你越界访问了,不管你是读还是写,也不管你在哪个位置越界的,都会检查出来。
也就是说,std::array的越界访问(函数调用 operator[])相较于C数组的越界检查(指针解引用)更严格。
模板的特化分为函数模板的特化和类模板的特化,具体如下:
2. 函数模板的特化
函数模板的特化步骤:
- 必须要先有一个基础的函数模板,特化不能单独存在;
- 关键字 template 后面接一对空的尖括号<>;
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型;
- 函数形参表:必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
首先我们来看看,为什么会有特化这个概念呢?即特化的应用场景是什么呢?如下:
class Date
{
public:
// 日期的构造函数, 年 月 日
Date(size_t year = 0, size_t month = 0, size_t day = 0)
:_year(year)
, _month(month)
, _day(day)
{}
// 对>的重载
bool operator>(const Date& d) const
{
if (((*this)._year > d._year)
|| ((*this)._year == d._year && (*this)._month > d._month)
|| ((*this)._year == d._year && (*this)._month == d._month && (*this)._day > d._day))
return true;
else
return false;
}
private:
size_t _year;
size_t _month;
size_t _day;
};
template<typename T>
bool greater (T left, T right)
{
return left > right;
}
}
void Test6(void)
{
cout << Xq::greater(20,10) << endl;
//预期结果为1
Xq::Date d1(2023, 8, 14);
Xq::Date d2(2023, 9, 10);
cout << Xq::greater(d1, d2) << endl;
//预期结果为0
cout << Xq::greater(&d1, &d2) << endl;
//预期结果为0
}
结果:
1
0
1
第三个结果与预期不符;咦,为什么?
- 第一次调用,传递的参数为两个整形,函授模板会实例化成整形,整形的比较没问题;
- 第二次调用,传递两个日期类对象,函数模板会实例化成两个日期类对象,调用日期类的operator>,结果没问题;
- 第三此调用,传递的是两个日期类对象的地址,但是函数模板会实例化成两个整形(因为地址其实就是一个整型数字),greater 会按照数字的大小比较,因此才会出现错误的结果。
这里就有一个问题,我们在这里不想让第三个比较以整形的方式进行比较,而是想让这个地址里面的内容按照特定的方式进行比较,这就叫做特化;
特化:针对某些类型进行特殊化处理;
template<class T>
bool greater( T left, T right)
{
return left > right;
}
//函数模板的特化
template<>
bool greater<Date*>( Date* left, Date* right)
{
return *left > *right;
}
再次调用上面的Test6(),结果:
1
0
0
3. 类模板的特化
namespace Xq
{
class Date
{
public:
Date(size_t year = 0, size_t month = 0, size_t day = 0)
:_year(year)
, _month(month)
, _day(day)
{}
bool operator>(const Date& d) const
{
if (((*this)._year > d._year)
|| ((*this)._year == d._year && (*this)._month > d._month)
|| ((*this)._year == d._year && (*this)._month == d._month && (*this)._day > d._day))
return true;
else
return false;
}
private:
size_t _year;
size_t _month;
size_t _day;
};
//仿函数
template<class T>
class greater
{
public:
bool operator()(const T& left, const T& right)
{
return left > right;
}
};
}
void Test7(void)
{
Xq::Date d1(2023, 8, 14);
Xq::Date d2(2023, 9, 13);
cout << Xq::greater<Xq::Date>()(d1, d2) << endl;
//预期结果为0
cout << Xq::greater<Xq::Date*>()(&d1, &d2) << endl;
//预期结果为0
}
结果:
0
1类模板和函数模板存在着同样的问题,当对于某些数据,我们需要进行特殊化处理;只不过函数模板和类模板的特化在形式上有所差别;
template<class T>
class greater
{
public:
bool operator()(const T& left, const T& right)
{
return left > right;
}
};
// 类模板的特化
// 在这里针对Date* 进行特殊化处理
template<>
class greater<Date*>
{
public:
bool operator()(Date* left, Date* right)
{
return *left > *right;
}
};
重新调用Test7,结果:
0
0
std::priority_queue<Xq::Date*, std::vector<Xq::Date*>, Xq::greater<Xq::Date*>> pq;
pq.push(new Xq::Date(2023, 8, 14));
pq.push(new Xq::Date(2023, 8, 13));
pq.push(new Xq::Date(2023, 8, 15));
pq.push(new Xq::Date(2023, 8, 12));
pq.push(new Xq::Date(2023, 8, 16));
pq.push(new Xq::Date(2023, 8, 8));
pq.push(new Xq::Date(2023, 8, 30));
如果我们没有对Xq::greater这个类进行特化(因为Xq::greate比较的时候是按照整形规则进行比较的),那么结果就是这样:
可以看到这不是堆;如果实现了特化,那么在进行push数据的时候, 会根据Date*的特殊化处理进行比较,得到结果的就是这样:
结果是一个小堆;特化就是针对某种类型进行特殊化处理;
3.1. 全特化
全特化即是将模板参数列表中所有的参数都确定化。 如下:
namespace Xq
{
template<class T1,class T2>
class A
{
public:
A(){cout << "A<T1,T2>" << endl;}
};
//全特化
template<>
class A<int,int>
{
public:
A(){cout << "A<int,int>" << endl;}
};
//全特化
template<>
class A<int,char>
{
public:
A(){cout << "A<int,char>" << endl;}
};
}
测试代码如下:
void Test4(void)
{
Xq::A<double, double> a1;
Xq::A<int, int> a2;
Xq::A<int, char> a3;
}
现象如下:
3.2. 偏特化
偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。比如对于以下模板类:
偏特化有以下两种表现方式:
3.2.1. 部分特化
部分特化的 demo,测试如下:
namespace Xq
{
template<class T1, class T2>
class A
{
public:
A(){ std::cout << "A<T1,T2>" << std::endl; }
};
// 偏特化
// 部分特化:将模板参数类表中的一部分参数特化。
template<class T>
class A<T, char>
{
public:
A(){ std::cout << "A<T,char>" << std::endl; }
};
}
void Test4(void)
{
Xq::A<char,char> a1;
Xq::A<double,char> a2;
}
现象如下:
3.2.2. 参数更进一步的限制
偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
偏特化中的参数更进一步的限制的测试 demo 如下:
namespace Xq
{
template<class T1, class T2>
class A
{
public:
A(){ std::cout << "A<T1,T2>" << std::endl; }
};
// 注意这里也是偏特化
template<class T1, class T2>
// 两个参数偏特化为指针类型
class A<T1*, T2*>
{
public:
A(){ std::cout << "A<T1*,T2*>" << std::endl; }
};
}
void Test5(void)
{
// 只要你的两个模板参数是指针都会匹配上面的偏特化
Xq::A<int*, int*> a1;
Xq::A<std::vector<int>*, int*> a2;
Xq::A<double*, char*> a3;
}
测试结果如下:
用户也可以将类模板参数偏特化为引用类型,如下:
namespace Xq
{
template<class T1, class T2>
class A
{
public:
A(){ std::cout << "A<T1,T2>" << std::endl; }
};
// 注意, 这里是偏特化
template<class T1, class T2>
// 两个参数偏特化为引用类型
class A<T1&, T2&>
{
public:
A(){ std::cout << "A<T1&,T2&>" << std::endl; }
};
}
void Test6(void)
{
//只要你显示传递模板参数是引用就会匹配上面的偏特化
Xq::A<int&, int&> a1;
Xq::A<std::string&, std::vector<int>&> a2;
Xq::A<double&, char&> a3;
}
现象如下:
3. 模板的分离编译
3.1 什么是分离编译
一个程序(项目)由若干个源文件共同组成,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
3.2 模板的分离编译
假如有以下场景,模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:
3.2.1. 头文件
下面的文件是一个头文件,里面只有 insert 和 push_back 这两个接口是声明和定义分离的 (在该文件下只包含这两个接口的声明),如下:
# pragma once
#include <iostream>
#include <assert.h>
#include <vector>
#include <algorithm>
using std::cout;
using std::endl;
namespace Xq
{
template<class T>
class vector
{
public:
typedef T* iterator;
vector()
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{}
~vector() { /* 实现省略 */ }
size_t size() const { /* 实现省略 */ }
void reserve(size_t n) { /* 实现省略 */ }
size_t capacity() const { /* 实现省略 */ }
T& operator[](size_t pos) { /* 实现省略 */ }
// 只是声明
void Xq::vector<T>::push_back(const T& val);
// 只是声明
iterator insert(iterator pos, const T& val);
private:
iterator _start;
iterator _finish;
iterator _end_of_storage;
};
}
3.2.2. 定义函数的源文件
这里面包含了 insert 和 push_back 两个接口的定义:
#include "my_vector.h"
template<class T>
void Xq::vector<T>::push_back(const T& val)
{
// 检查容量
if (_end_of_storage == _finish)
{
reserve(capacity() == 0 ? 4 : 2 * capacity());
}
*_finish = val;
++_finish;
}
template<class T>
typename Xq::vector<T>::iterator Xq::vector<T>::insert(typename Xq::vector<T>::iterator pos, const T& val)
{
// 1. 检查pos的合法性
assert(pos >= _start && pos <= _finish);
// 2. 检查容量
if (_finish == _end_of_storage)
{
// 提前算好pos的位置
size_t n = pos - _start;
reserve(capacity() == 0 ? 4 : 2 * capacity());
// 更新 pos
pos = _start + n;
}
iterator end = _finish;
while (end > pos)
{
*end = *(end - 1);
--end;
}
*end = val;
++_finish;
return pos;
}
3.2.3. 测试源文件
#include "my_vector.h"
void Test8(void)
{
Xq::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
for (size_t i = 0; i < v.size(); ++i)
{
cout << v[i] << " ";
}
cout << endl;
}
int main()
{
Test8();
}
首先我们分析一下定义函数的源文件的一些细节:
因为push_back在Xq这个命名空间里,其次类里面加了模板参数,因此在这里也要加上模板参数,如下:
template<class T>
void Xq::vector<T>::push_back(const T& val)
{
// 实现省略
}
insert 接口也是同理,需要加入命名空间以及模板参数,如下:
template<class T>
typename Xq::vector<T>::iterator Xq::vector<T>::insert(typename Xq::vector<T>::iterator pos, const T& val)
{
// 实现省略
}
这里为什么要加上typename呢?
当编译代码时,会发生编译报错,原因是:iterator是属于这个类模板 (Xq::vector<T>) 的内嵌类型,当编译器进行编译的时候,编译器不知道这里的 iterator 是类型还是变量 (因为类的静态变量指定类域即可访问),因此我们要在这里加上typename,即告诉编译器这里的 iterator 是类型而不是变量;
OK,回归正题,当我们调用Test8()这个测试文件时,会得到下面的结果:
这是一个链接错误,一般情况下,链接错误的原因无非就是有声明找不到定义;
在这里,我们发现,例如构造,析构,size(),operaotr[],都没有报错,而 push_back 却报了编译错误,它们的差别是什么呢?
前者声明和定义没有分离,在同一个文件里面;
后者声明在头文件里,定义却在定义函数的源文件里面;
因此我们得到造成这种问题的原因就是因为push_back的声明和定义分离导致链接错误,具体如下:
3.2.4. 解决方案 (模板的分离编译带来的链接错误)
有两种解决方案,如下:
- 解决方案一: 模板声明和定义不要分离 (在有模板的前提下,不要将声明和定义写在两个文件里),而是将声明和定义放到一个文件里 (xxx.hpp / xxx.h都可以),推荐使用这种方式;
- 解决方案二:模板定义的位置显式实例化。但这种方式不实用,不推荐使用。
解决方案二的测试demo 如下:
#include "my_vector.h"
template<class T>
void Xq::vector<T>::push_back(const T& val)
{
// 实现省略
}
template<class T>
typename Xq::vector<T>::iterator
Xq::vector<T>::insert(typename Xq::vector<T>::iterator pos, const T& val)
{
// 实现省略
}
// 模板显式实例化
template Xq::vector<int>;
现象如下:
虽然上面成功运行,但又诞生了一个问题,就是写死了,如果我们此时需要存储另一个类型的数据,如下:
因此,第二种解决方案局限性太强 (换一种不同的类型,就需要显式实例化一次),不推荐使用,更推荐第一种解决方案,即在有模板的前提下,将接口的声明和实现放在一起。
4. 模板总结
优点:
- 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生;
- 增强了代码的灵活性 (在这里是将重复的事情交给了编译器做)。
缺点:
- 模板会导致代码膨胀问题,也会导致编译时间变长 (增加了模板实例化这个过程);
- 出现模板编译错误时,错误信息非常紊乱,不易定位错误。