前言
模板是搭建容器的基本工具,也是泛型编程思想的代表。
模板用的好,效率低不了!
模板可以提高程序的灵活性,便于高效的迭代开发。
在之前的文章中,我们学习了模板替代类型的功能。
现在,我们再来学习一些有关模板的更高级的操作。
一.非类型模板参数
在之前的学习中,我们的模板参数都是用来匹配不同的类型的,eg:int、double、float等。但,除了能够匹配这些类型之外,我们还可以使用模板来匹配常量。
1.1使用方法
匹配非类型模板参数时,我们不用再使用class/typename.,而是直接使用具体的类型。
例如:我们可以使用非类型模板参数定义一个大小可以自由调节的整数数组类。
如下:
//template<typename T>
template<size_t N>
class arr
{
public:
size_t size() const
{
return N;
}
protected:
size_t _arr[N];//长度为N的无符号整型数组
};
int main()
{
arr<10> a1;
arr<100> a2;
arr<1000> a3;
cout << a1.size() << endl << a2.size() << endl << a3.size() << endl;
return 0;
}
运行结果如下:
10
100
1000
PS:非类型模板参数必须是常量,这个常量是在编译阶段确认的。
在一个模板中,类型模板参数和非类型模板参数是允许同时存在的,因此,我们可以通过加一个模板参数使数组存储的类型自定义,如下:
template<class T,size_t N>
class arr
{
public:
size_t size() const
{
return N;
}
protected:
T _arr[N];//长度为N的无符号整型数组
};
这样,我们便得到了一个类型自定义的数组模板。
下面,我们使用一下:
int main()
{
arr<int, 10> a1;
arr<double, 10> a2;
arr<vector<int>, 10> a3;
cout << typeid(a1).name() << endl;
cout << typeid(a2).name() << endl;
cout << typeid(a3).name() << endl;
return 0;
}
打印结果如下:
class arr<int,10>
class arr<double,10>
class arr<class std::vector<int,class std::allocator<int> >,10>
非类型模板参数是允许缺省的,因此下面这行语句也是合法的
template<class T, size_t N=100>
这样的话,我们在定义时则可以省略缺省值,如下:
arr<int> a4;
cout << typeid(a4).name() << endl;
打印结果如下:
class arr<int,100>
知识点补充:
- typeid是一个运算符,返回的是一个名为std::type_info的引用。
- 这个类在标准库中,在这个类中有一个函数为name()。
- 我们调用这个函数,即可获得类型的名字。
1.2类型要求
刚刚我们已经见识到了非类型模板参数的神奇之处,但,同时,他也有局限性。
如下:
TYD,这怎么报错了呢?
答案:
- 非类型模板参数只能使用整数家族当中的类型,其他的类型是非法的。(C++20中,允许double类型)
因此,我们便可以总结出非类型模板参数的使用条件:
- 非类型模板参数必须是常量,而且需要在编译阶段确定。
- 非类型模板参数只能是整型家族,其他类型不允许。
整型家族:short、long、long long、int、char、bool等。
1.3举例说明
下面,我们通过CPP官网中的一个例子来体验一下:
在C++11中,引入了一个新的容器array,它的定义中就使用了非类型模板参数。是一个真正意义上的泛型数组。
array的第二个参数就是非类型模板参数。
它包含在头文件<array>中。
定义方式如下:
array<int, 10> a1;
cout << typeid(a1).name() << endl;
array是泛型编程思想的产物,它支持非常多的STL容器的功能,譬如迭代器和运算符重载等。而它对比传统的数组而言,最大的进步是其严格的检查了越界行为。(读写)
//vs2019环境下
int OldArr[10] = { 0 };//传统数组
array<int, 10> NewArr = { 0 };//新数组
OldArr[15];//传统数组-->不报错
NewArr[15];//新数组-->报错
OldArr[12] = 0;//传统数组->越界写,不报错。
NewArr[12] = 0;//新数组->越界写,报错。
return 0;
旧数组虽然也会进行越界检查,但是其检查的不严格,新数组对越界检查的很严格。
新数组虽然有诸如以上的优点,但在实际的开发中,却没多少人用它,这是为什么呢?
- array对标传统数组,没有初始化。
- array对标vector,功能和实用性被全方位碾压
- array开辟在栈上,存在栈溢出的问题;而vector开辟在堆上,不存在该问题。
那么,array是如何做到的严格检查呢?
很简单,只要是用下标我们就assert一下就好了。eg:assert(pos>=0&&pos<Max)
二.模板特化
模板除了可以根据传入的类型进行实例化外,还可以指定实例化。
打个比方:
我穿越到了2300年,乌烟瘴气。
人类无法生存,于是乎,高科技振臂一呼:“无所谓,我会出手。”
从此,人类便可以进行光合作用了。
普通人只能活在蓝天白云下,但,高科技改造后,在乌烟瘴气中也如鱼得水。
模板特化的意义便在于此,对泛型思想的特殊化处理。以便更符合我们的使用需求。
2.1概念
一般情况下,模板是可以帮助我们处理一大批与类型无关的代码的,但在某些场景中。[泛型]便无法再满足调用方的需求了,此时会引发错误。
eg:我们在用指针构建优先级队列后,若不编写相对应的仿函数,则比较结果是未知的。
int main()
{
//结果正常
priority_queue<int> pq1;
pq1.push(int(1));
pq1.push(int(3));
pq1.push(int(2));
cout << pq1.top() << endl;
//结果不正常 //在vs中,会优先使用连续的空间,因此1 4 3的空间是挨着的,因此大概率3的地址最高
//若地址不连续,则结果是不可知的。
priority_queue<int*> pq2;
pq2.push(new int(1));
pq2.push(new int(4));
pq2.push(new int(3));
cout << *pq2.top() << endl;
return 0;
}
出现结果未定义的原因是,我们进行的是地址的比较,而不是值的比较。
在上面的这个例子中,我们发现结果是未定义的。
原因:泛型思想无法满足一些特殊场景
解决方法:通过模板的特化,对特殊场景做出特殊处理。
2.2函数模板特化
函数也可以使用模板,因此也是支持模板的特化的。
下面我们写一个函数:
template<class T>
bool IsEqual(T x, T y)
{
return x == y;
}
int main()
{
int a = 10;
int b = 10;
cout << IsEqual(a, b) << endl;
char str1[] = "kuzi";
char str2[] = "kuzi";
cout << IsEqual(str1,str2) << endl;
return 0;
}
结果如下:
1
0
我们发现,字符串的比较结果为假。这显然不是我们想要的结果。
这是因为,字符串比较时,比较的是地址,而不是内容。
因此,我们就需要利用模板的特化为字符串的比较构建出一个特殊模板。
template<>
bool IsEqual<char*>(char* x, char* y)
{
return strcmp(x, y) == 0;
}
这时,即可完成我们对模板的比较。
但,其实,函数重载更好用。因此我们日常的开发中还是用函数重载比较多,基本上没人用函数的特化。
2.3类模板特化
模板特化主要还是用于类模板,它可以在在泛型思想的基础上解决大部分特殊场景。
类模板特化分为两种:全特化/偏特化
全特化和偏特化对应不同的场景。
2.3.1 全特化
全特化是指将所有的模板参数特化为具体类型,将模板全特化后,调用时会优先选择更为匹配的模板类。
如下:
template<class T1,class T2>
class Test
{
public:
Test(const T1 &t1,const T2 &t2)
:_T1(t1)
,_T2(t2)
{
cout << "template<class T1,class T2>" << endl;
}
protected:
T1 _T1;
T2 _T2;
};
template <>
class Test<int, int>
{
public:
Test(const int& t1, const int& t2)
:_T1(t1)
,_T2(t2)
{
cout << "template<int T1,int T2>" << endl;
}
protected:
int _T1;
int _T2;
};
int main()
{
Test<int, char> T1(1, 2.3);
Test<int, int> T2(1, 2);
}
结果:
- 在我们对模板全特化后,实际调用时,会优先选择匹配度更高的模板调用。
2.3.2 偏特化
既然,全特化能让模板参数全部都用别的类型取代,那么,是不是也可以只取代一个参数呢?
答案是可以的。
偏特化:将泛型的范围进一步缩小,可以限制为具体的某种类型(包括指针)。
template<class T1,class T2>
class Test
{
public:
Test()
{
cout << "class Test<class T1,class T2>" << endl;
}
};
template <class T>
class Test<T, int>
{
public:
Test()
{
cout << "class Test<class T1,int>" << endl;
}
};
template <class T>
class Test<T, T*>
{
public:
Test()
{
cout << "class Test<class T,class T*>" << endl;
}
};
int main()
{
//1
Test<float, double> t1;
//2 模板参数是严格按照顺序匹配的。注意甄别。
Test<int, double> t2;
Test<double, int> t3;
//3
Test<int, int*> t4;
return 0;
}
打印结果:
class Test<class T1,class T2>
class Test<class T1,class T2>
class Test<class T1,int>
class Test<class T,class T*>
我们要注意的一点是,模板参数的匹配是严格按照顺序匹配的。
我们把2中的代码拿出来和模板单独对比一下:
class Test<T, int>
Test<int, double> t2;
Test<double, int> t3;
模板中,int为第二个参数,因此,只有t3使用了这个模板,而t2则由于顺序不同而匹配不上。
偏特化在泛型思想和特殊情况之间做了折中的处理,使得限制范围的偏特化也可以实现泛型。
- 偏特化为T*,那么传入int*、char*等都是可行的。
借助偏特化可以解决指针无法正常比较问题:
template <class T>
class Less
{
public:
bool operator ()(T x, T y) const//重载(),仿函数->让类可以和函数一样使用。
{
return x < y;
}
};
//偏特化
template <class T>
class Less<T*>
{
bool operator ()(T* x, T* y) const//重载(),仿函数->让类可以和函数一样使用。
{
return *x < *y;
}
};
//下面这个是全特化,一定要注意区分。
template <>
class Less<int>
{
bool operator ()(int x, int y) const//重载(),仿函数->让类可以和函数一样使用。
{
return x < y;
}
}
注意点:
- 在偏特化之前,一定要已经存在泛型模板
- 不要把偏特化和全特化搞混!!!(血的教训!)
下面,我们写一个全特化的类和偏特化的类
三.模板的分离编译问题
在之前已经写了一篇有关模板的文章,在那篇文章中,我们提到了模板是不能分离编译的,会发生链接错误。
下面,我们来谈一谈这个问题。
3.1失败原因
声明与定义分离后,在进行链接时,将无法在保存函数地址的符号表中找到目标函数的地址,因此链接错误。
当模板的声明和定义分离时,因为是泛型的,他不是具体的,因此编译器便无法确认函数具体长什么样子。也就是无法生成函数,也就自然不能获得到函数的地址了,没有地址也自然不会将函数的地址存到符号表中了。但,链接是要去符号表中找函数地址进行链接的,因此出现了错误。
3.2解决方法
既然说,泛型的无法生成地址,那么,是不是特化后的就能够生成地址了呢?
答案是正确的,我们如果在函数定义时时候进行模板特化,编译时就可以生成地址并进行链接。
但,这样子的话,我们要特化特别多份,而且其远远不如函数重载好用。
那么,我们有没有别的办法可以处理这个问题呢?
自然是可以的,我们可以从根本上解决这个问题!
既然不让分离编译,那么我们就直接将模板的声明和定义放在同一个文件中不就可以了嘛。
因此,我们可以总结出如下两种解决方法:
- 函数定义时进行模板特化,生成地址进行链接(不推荐)
- 模板的声明和定义不要分离,直接写进同一个文件中。
下面补充一些关于模板声明定义放到一起的知识
- STL中都是声明和定义放在同一个文件夹中
- 为了让别人看出头文件中包含了声明和定义,可以将头文件的后缀改为.hpp.在Boost库中就采取了这样的命名方式。
4.模板总结
优点:
- 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
- 增强了代码的灵活性
缺点:
3. 模板会导致代码膨胀问题,也会导致编译时间变长
4. 出现模板编译错误时,错误信息非常凌乱,不易定位错误
模板是一把双刃剑,用好了,它将会是一把锐利的武器,用的不好,它将成为压倒你的大山。