Bootstrap

C++中模板的进一步理解

一、非类型模板参数

1.1本质

非类型模板参数本质是一种常量,他的底层和其他模板参数一样,都是交给编译器实现多个类

1.2出现原因

假设我们要实现两个静态栈,他们的模板类型一样但是空间不一样:

stack<int> st1;//希望空间大小是15

stack<int> st2;//希望空间大小是200

那么我们在定义栈的时候用#define来定义一个符号代表大小,这个大小该多少合适呢?很明显,15或200都是不合理的

因此,提出了非类型模板参数的概念:(支持缺省参数)

template<class T,size_t N=4>

stack<int,15> st1;//希望空间大小是15

stack<int,200> st2;//希望空间大小是200

1.3非类型模板参数允许的类型范围

C++20之前只支持整形作为非类型模板参数,但之后就可以支持非整形(如double,float,int*等等,可以推广到内置类型),但是自定义类型还是不支持的

1.4封装静态数组的类:array

1.4.1模板参数和使用方式

array就采用了非类型模板参数,它的模板参数有

<class T,sie_t N>

使用的时候可以

array<int,10> aa;

1.4.2出现原因

普通数组对于越界的检查方式都是“抽查”(往往只能查最大值附近),如

a[10] = { 0 };

a[10] = 1;//此时会直接报错

a[13] = 2;//此时往往不会报错

例子中是修改数据,如果只是访问数组中数据,报错的概率会更低,如打印数组数据时的访问

1.4.2补:

位图bitset也会用到非类型模板参数

1.4.3缺点

①array完全可以用vector来代替

②array创建完成后并不会直接对数组空间初始化

③array是在当前栈中开辟空间,而不是在堆里,例如要存10个int类型的1,

array的sizeof结果会是40;vector的sizeof结果却只是16(因为指针的缘故,在32位下的结果),

而栈的空间十分有限

1.4补:C++11中的“一切皆可花括号赋值”

C++11支持了很多情况下的{}直接赋值,如

vector<int> v1={1,2,3,5};
vector<double> v2={1.1,2.2,3.3,5.5};

//下面三个效果一样
int i=1;
int j={1};
int k{1};

当然也包括多参数构造函数使用的{}

1.5关于实现不同模板类型对应vector的打印

1.5.1解决方法

显式实现打印函数的时候用模板即可,但需要注意在定义首位迭代器的时候必须

typename vector<T>::const_iterator it=v.begin(); 

1.5.2原因

类模板在没有实例化的时候,编译器不会去查里面的细节内容

而在类的外面出现vector<T>::const_iterator这种指定方式对应了两种可能:

①静态成员变量在类外的访问

②一种类型,用来创建变量

只有在指定了typename之后,明确其为“一种类型名称”,来让编译器实例化时再查看

(当然,定义首位迭代器时也可以用auto关键字来避开这一问题)

1.5补:取别名相关问题

引用是给变量起别名

typedef是给类型起别名

#define是给常量起别名

注意不要混淆

二、模板的全特化与偏特化

2.1模板特化的本质

其实是对特定模板类型的特殊化处理

2.2函数模板的全特化

2.2.1使用举例

假如我预先准备了一个比较大小的模板

template <class T>
bool less(T left, T right)
{
    return left<right;
}

此时我实现了一个日期类Date,并重载了它的‘ < ’符号,此时我如果希望传入两个Date*类型的指针也能比较出Date本身的大小,我该怎么做呢?

此时便可以使用函数模板的特化:

template <Date*>
bool less(Date* left, Date* right)
{
    return *left < *right;
}

2.2.2不推荐使用的原因

其实函数模板特化的坑很多,比如const修饰下,指针的特化

如果原模版函数是这样的:

template <class T>
void func(const T& t1,const T& t2)
{}

我们在特化Date*的时候是否是这样呢?

template <Date*>
void func(const Date*& t1,const Date*& t2)
{}

仔细分析我们就会发现,这样是错误的:特化中的const修饰的是Date*指向的内容,但实际在原模版函数中,我们的const是用来修饰Date*本身的,因此我们需要修改为这样

template <Date*>
void func(Date* const& t1,Date* const& t2)
{}

可以发现与习惯性思维完全不同,很容易出错

2.3类模板的全特化

使用举例:

template <class T1, class T2>
class A
{
    //...
protected:
    T1 _a;
    T2 _b;
}


//特化的时候
template <>
class A<int , char>
{//...}

再比如之前利用仿函数来实现区分大小堆的时候,最后提到用一个新的类作为仿函数完成对指针指向内容的比较大小,我们完全可以对已有仿函数进行特化

2.3补:

对于类模板,我们也需要注意当const修饰指针时候const的位置问题

2.4偏特化(半特化)

2.4.1特化部分模板参数

如:

template <class T>
class A<T , char>
{//...}

2.4.2特化指针

这是一种比较特殊的特化,可以达到只要传指针都走他的目的

template <typename T1, typename T2>
class A<T1* , T2*>
{//...}

在使用的时候,如果传参

int** ppa;

double* pb;

A(ppa, pb);

再对T1进行typeid打印名字,会出现:int*

T2的打印会出现:double

而sizeof的结果也是对应int*与double的大小

2.4.3特化引用

如:

template <typename T1, typename T2>
class A<T1& , T2&>
{//...}

效果与指针极其相似,T1与T2对应的仍然是原类型,只有在传参都是引用类型的别名时起作用。

2.4补:

指针和引用的特化比较特殊,而且我们除了单独特化,也可以把两者混搭起来进行特化

三、模板的声明与定义分离

3.1函数模板

3.1.1与一般函数声明与定义分离的区别

普通函数在调用时,从汇编层面看会call一个地址,这个地址对应了函数实现中第一句语句的位置

但函数模板因为没有被实例化,所以不会被编译,不会生成指令,也不会有地址放到符号表中这一过程

假设我们实现一个项目,有三个文件:

①Func.h     ->在此处声明函数模板

②Func.cpp     ->在此处实现函数模板

③Test.cpp     ->在此处调用模板函数

如果我在Test.cpp中调用该函数,而此时我只包含了Func.h头文件,(我们都知道,在进行链接过程之前,各个.cpp文件之间都是互相不可见的)我就会因为编译的时候知道实例化成什么,但是没有定义而无法编译通过;此时Func.cpp中,我虽然有了函数模板的定义,但是我不知道应该实例化成什么而无法编译通过,导致同时报两个错

3.1.1补:普通函数实现过程

普通函数实现时会被编译,生成指令,函数的地址也会放到符号表里

3.1.2解决方案:总结可知他们的问题是两个.cpp中,定义与实例化之间缺少交互

①方式一:在模板定义的地方显式实例化,完成两者交互

template <class T>
T ADD(const T& left, const T& right)
{//...}

template 
int ADD(const int& left, const int& right);

template 
Date ADD(const Date& left, const Date& right);

但是这种方式需要我们反复补充实例化,需要知道都用到什么什么类型,故不推荐使用

②方法二:声明与定义不要分离到两个文件,把定义全都放在Func.h中,定义可以被实例化找到,自然可以直接完成交互

3.1补:.hpp文件

是.h文件和.cpp文件的聚合体,通常用于模板书写+实例化调用

3.2类模板

方法:

实现的时候,短的成员函数直接放在类的内部,长的可以放到外部,外部定义的时候带上类模板的模板参数即可:

假如有类A:

template <class T1,calss T2>
class A
{//...}

此时要在类外实现函数func

template <class T1,calss T2>
void A<T1,T2>::func(const T1& t1,const T2& t2)
{//...}

四、对于模板的总结

4.1优点

①实现了代码的复用,减少了书写成本,可以更快的进行开发且C++标准库STL也因此产生

②增强了代码的灵活性

4.2缺点

①本质上是把代码生成的任务交给编译器,会导致代码膨胀的问题,导致编译时间加长

②一旦出现编译出错的问题,报错会十分杂乱无章

;