再谈模板
1、非类型模板参数
在我们之前的学习中,模板可以传类型参数。
其实模板还可以传类型之外的参数:

模板传参,支持:
- 传递类型形参
- 传递非类型形参
- C++20前,只能传整型常量
那么非类型传参有什么用呢?
假设我们想要一个静态的stack。
如果我们不用非类型传参,要想得到一个存10个元素的静态stack,我们就可以这样写:
cpp
template<class T>
class my_stack
{
public:
private:
T a[10];// 10个元素
int _top;
};
现在我们有了新的需求:想得到一个存1000个元素的静态stack。
我们就必须直接改代码,或者再声明一个stack类。太麻烦了。
所以支持非类型传参的模板,就派上了用场:
cpp
template<class T, size_t n = 10>
class my_stack
{
public:
private:
T a[n];
int _top;
};

需要注意的是:
1、传入非类型参数n的,只能是常量:
2、非类型参数在C++20之前只能传整型值,虽然C++20之后放开了限制,但是依旧不能传一些类型,比如string。
1.1、array
C++的有些类模板是使用了非类型参数的,比如静态数组array:

我们可以发现,array就不支持诸如push_back, pop_front等操作了,而是支持重载[ ],因为它的容量是固定的,只需下标访问就可以修改。
array支持迭代器。
指向数组的指针,是天然的迭代器。
array对于内置类型作参数,默认不会初始化:

我们可以调用其中的方法fill(),手动初始化:

array有什么作用呢?有C语言的数组不就行了?
我们可以通过对比array和C语言的数组,来找出array的优势:
1、越界访问
- C语言越界访问的检查行为是抽查,而且只能检查写时的访问,不能检查读时的访问。
- array的访问行为是经过重载的,重载函数体内可以进行严格的检查。
2、作其它容器的类型参数
- array可以作其它容器的类型参数:
3、传参的退化问题
- C语言数组传参会退化为指针,会出现一些问题,比如使用不了范围for:
- array无需担心,甚至可以传引用提高效率:
2、模板的特化
模板的特化,就是对模板作特殊化处理。
2.1、函数模板的特化
比如对于比较日期大小的函数的特化。
cpp
template<class T>
bool DateLess(T x, T y)
{
return x < y;
}
直接比较Date,没有问题:

如果直接比较Date*,可能会有问题:

那么我们就可以将DateLess进行特化。
cpp
template<>// 特化:专门给Date*使用
bool DateLess<Date*>(Date* x, Date* y)
{
return *x < *y;
}
对于函数模板的特化,我们有两点需要注意:
1、const引用的使用
对于原DateLess,我们期望加上const引用,以减少拷贝:
而我们依葫芦画瓢,为专门给Date*使用的特化DateLess,加上const引用:
编译报错了。
首先,我们之前学过,const修饰指针,有两种情况:
const int* p1:const修饰*p1,即指针指向的内容int* const p1:const修饰p1,即指针对于原DateLess,传入的是Date类型,那么const修饰的是Date类型。
那么相对的,对于专门给 Date* 使用的特化DateLess,const修饰的应该是 Date* 类型(指针)。
所以应该这样写:
2、不特化,而匹配
对于函数模板,我们可以直接写一个匹配度高的普通函数,从而不使用函数模板:
- "吃现成的":有现成的函数,就不使用函数模板。
- "吃好吃的":有更匹配的参数类型,就不使用其它的类型,以避免隐式类型转换。
2.2、类模板的特化
特化:specialization
cpp
template<class T1, class T2>
class A
{
public:
A() :_a1(0),_a2(0) { cout << "A<T1, T2>" << endl; }
private:
T1 _a1;
T2 _a2;
};
template<>// 特化
class A<int, int>
{
public:
A() :_a1(0) { cout << "A<int, int>" << endl; }
private:
int _a1;
};

类模板的特化,对内部成员没有要求,可以加,可以删:
cpp
template<class T1, class T2>
class A
{
public:
A() :_a1(0),_a2(0) { cout << "A<T1, T2>" << endl; }
private:
T1 _a1;
T2 _a2;
};
template<>// 特化
class A<int, int>
{
public:
A() :_a1(0) { cout << "A<int, int>" << endl; }
void func1() {}// 添加func1()
private:
int _a1;// 删除_a2
};
vector< bool >就使用了类模板的特化:
我们可以简单理解为:一般的vector存int(0, 1)表示true, false,需要4个字节。
但是vector< bool >进行了空间优化的处理,只用一个bit的空间来表示true, false。
2.2.1、全特化与偏特化(半特化)
全特化:所有的模板参数进行特化。
cpp
template<class T1, class T2>
class A
{
// ...
};
template<>// 全特化
class A<int, int>
{
// ...
};
偏特化比较复杂。
偏特化可以是固定部分类型参数:
cpp
template<class T1, class T2>
class A
{
public:
A() :_a1(0), _a2(0) { cout << "A<T1, T2>" << endl; }
};
template<>
class A<int, int>// 全特化
{
public:
A() :_a1(0) { cout << "A<int, int>" << endl; }
};
template<class T>
class A<T, int>// 偏特化
{
public:
A() { cout << "A<T, int>" << endl; }
};

偏特化也可以是限制实例化出的类的参数:
cpp
template<class T1, class T2>
class A<T1*, T2*>
{
public:
A() { cout << "A<T1*, T2*>" << endl; }
};
我们还可以发现,上面的模板,还可以根据传入的指针类型,推导出T1, T2:
cpp
template<class T1, class T2>
class A<T1*, T2*>
{
public:
A() { cout << "A<T1*, T2*>" << endl; }
void func1() { cout << typeid(T1).name() << " " << typeid(T2).name() << endl; }
};

除了限制为指针、引用,还可以限制为指针+引用、指针+int、引用+int等。
2.3、特化的应用
我们可以对之前模拟实现的priority_queue作进一步优化,利用特化实现专门给Date*比较大小的仿函数:

我们这样写,又出问题了:


const修饰的是*left(*right),而引用修饰的是left, right,所以这里的引用为普通引用,而不是const引用。
push, pop需要调用调整算法,而调整算法使用了仿函数对象_com,_com接收的是 Date* 类型值,但是传入到 _com 的重载()时, Date* 类型值需隐式转换为const Date*&,而隐式转换产生临时拷贝,临时拷贝具有常性,权限放大:

两个解决办法:
1、再加一个const,变成const引用:

2、不使用引用:

3、模板的分离编译
模板不支持声明和定义直接分离到不同文件内:

要搞清楚这个问题,我们可以简单模拟编译器编译、链接的全过程。
现在我们有一个项目,里面有三个文件;
cpp
// a.h
#include<iostream>
using namespace std;
int Add(int x, int y);
template<class T>
T TAdd(T x, T y);
cpp
// a.cpp
#include"a.h"
int Add(int x, int y)
{
return x + y;
}
template<class T>
T TAdd(T x, T y)
{
return x + y;
}
cpp
// main.cpp
#include"a.h"
int main()
{
cout << Add(1, 2) << endl;
cout << TAdd(1, 2) << endl;
return 0;
}
预处理阶段:展开头文件、条件编译、宏替换、删除注释...
预处理结束,生成文件a.i main.i:

然后进入编译阶段:检查语法是否有错误。
如果语法没有问题,就将代码转化成汇编代码,生成文件a.s main.s:

这里我们不关注其他细节,只看main.s的函数调用部分。
函数调用用call指令。但是当前函数Add()和TAdd()的定义不在main.s处,所以找不到函数地址,按道理编译阶段是不会通过的。
但是存在函数的声明,编译阶段就通过了。
函数的声明,相当于提醒call指令,在链接阶段再找函数的地址。
接着来到汇编阶段 :将汇编代码转化为二进制机器码。最终生成文件:a.o main.o
汇编结束,进入链接阶段 :合并生成可执行程序,链接在其他文件定义的函数、变量 等。生成文件:a.out。
在这个过程中,会有一个符号表,上面记录着函数的地址。那么这时call指令就可以在这个符号表上寻找函数的地址。
问题就出在这里:TAdd函数的地址找不到。
因为TAdd没有实例化出函数。
而没有实例化,是因为直到多个文件合并成一个可执行程序的时候,编译器也没有向TAdd模板传入类型参数,因为TAdd模板的声明和定义是分离的,整个编译链接过程也是分开进行的。也就是说,TAdd不知道要实例化成什么函数。
所以,TAdd模板的分离编译会报错。
我们可以这么理解:
- 知道实例化的地方,只有声明没有定义
- 不知道实例化的地方,却有声明
- 结果是模板没有实例化出函数 / 类
有两种补救方案:
第一个是在a.cpp文件内显示实例化模板。但是遇到一种情况就必须显示写一个实例化函数,太麻烦了。
第二种方法,就是直接将模板直接定义在a.h。也是我们迄今为止一直在做的。











