【C++】模板进阶:非类型模板参数与模板的特化
🧑💻博主名称:鱼子星_
✅数据结构专栏:【数据结构】
✅算法竞赛专栏:【算法竞赛】
✅C++系列专栏:【C++从零开始系列】
目录
- 一. 非类型的模板参数
- 1. 非类型模板参数定义方式
- 2. 非类型模板参数的使用场景
- 二. 模板的特化
- 1. 函数模板的特化
- 2. 函数模板特化的定义
- 3. 函数模板特化与函数重载
- 2. 类模板的特化
- 2.1 类模板的全特化
- 2.2 类模板的偏特化
- 2.3 类模板特化的应用-仿函数的改造
- 三. 模板声明和定义不可分离原因
- 问题场景(定义和声明分离)
- 编译和链接过程发生了什么?
一. 非类型的模板参数
C++中的模板是泛型编程的核心,有了模板这个东西之后,减少了很多逻辑相似的代码,且STL也是基于模板才产生。由前面的学习我们知道,模板的参数大多是类型,而不是对象,这也符合编译器推断模板的类型这个规则。但是,模板参数并不是只能传递类型,还可以传递对象,指针和引用。
- 当某个对象作为模板参数时,实参只能传递常量而不能传递变量,且在模板实例化之后这个对象也为常量
- 当指针或引用作为模板参数时,实参只能传递生命周期为全局的指针或引用
1. 非类型模板参数定义方式
非类型的模板参数的定义和函数的形参的定义差不多。非类型的模板参数定义如下:
template<classT,size_t N>classstack{private:T _a[N];size_t _size;};对于模板类stack,其中的N为整型常量,而如果想要实例化这个模板类,第二个模板参数只能传递常量,而不能传递变量。
- ✅正确示范(非类型的,模板参数传递常量):
stack<int, 10> st; - ❌错误示范(非类型的,模板参数传递变量):
int a = 10; stack<int, a> st;
2. 非类型模板参数的使用场景
非类型模板参数的使用场景有2个,如下所示:
需要同时存在不同大小的静态栈
当程序中需要的是不同大小的静态栈时,此时对于模板类stack来说,就需要一个整型常量作为模板参数来控制静态栈的大小。
stack<int,100>st1;stack<int,1000>st2;std::array的定义
std::array文档
标准库中对于静态数组的封装(std::array)也用到了非类型的模板参数,如图1-1所示,其中非类型的模板参数的主要作用其实和场景1相似。
图 1-1 std::array的非类型模板参数
二. 模板的特化
模板可以通过编译器的推断使得所有相同样式但不同类型的调用都执行相同逻辑的操作,但是这种所有的相同样式的调用都执行一套逻辑也有缺陷。对于某些特殊的类型,其调用时并不希望和其它类型一样执行模板内部的操作,而是有其特殊的操作需求,对于这种情况,就需要使用模板的特化。
模板的特化(也叫特例化)即对一个模板进行特殊化的处理,使得某些特殊的类型调用到这个模板时和其它类型调用模板时执行不一样的操作/代码。
1. 函数模板的特化
下面来看一个普通的模板函数Compare,此函数用于比较两个相同类型的对象的大小。该函数可以用于大部分类型的对象进行比较,如int,double,string……但是,如果此时需要比较两个指向int类型对象的指针所指向对象的大小,这个比较函数就无法正确的对所指向对象进行比较,而是只会对指针的进行比较。
template<classT>boolCompare(constT&a,constT&b){returna<b;}2. 函数模板特化的定义
对于这种使用模板不能满足一些特殊类型的需求的情况,就需要对指针类型进行特化。
- 函数模板的特化需要在函数整体的之前加上关键字template和一对空的尖括号(
<>)来表示这是一个函数模板的特化 - 函数名和原先的函数名相同,特化的函数模板需要在函数名之后加上需要特化的类型,需要特化的类型需要使用尖括号(
<>)括起来 - 函数参数需要和原模板函数的参数一 一对应。
下面以整型作为需要特化的类型为例,假设我们的需求是:当使用Compare比较整型时,需要当a > b时才返回true,否则返回false。
template<>boolCompare<int>(constint&a,constint&b){returna>b;}我们真正所需要的还是指针类型的特化,但是需要注意的是,指针,引用,函数模板,特化这几个东西都参与之后会产生一些奇妙的效果。
template<>boolCompare<int*>(int*const&a,int*const&b){return*a<*b;}指针的特化的参数的解释:由于特化需要和原模版的参数一 一对应,原先模板参数为
const T& a,这个const修饰的是对象本身(即 a),所以对于指针类型的特化也应该让const修饰对象本身,所以const需要放在*之后,而因为原模版参数是对象的引用,所以指针的特化也需要加上引用,最终就形成了int* const &a。
3. 函数模板特化与函数重载
函数模板的特化本质上是原函数模板的实例,并不是创建了一个新的函数。
举个例子,小明是个跑腿专家,你给他几块钱他就会给你带回几瓶水来,你给他1块钱他会给你带1瓶矿泉水,给他3块钱就会带3瓶矿泉水,现在你给小明提出了新的要求,“如果我给你3块钱,你不要给我带3瓶矿泉水了,而是给我带一瓶可乐回来”,之后只要你给小明3块钱他就会带可乐,而其它价钱依旧是矿泉水。
这其中”给小明几块带几瓶矿泉水“就是函数原模版,而“给3块钱带可乐回来”只是在原先模板上限定了特例,就是模板的特化,也就是模板的实例。而如果是叫小红帮忙跑腿,那就是创建一个新的函数了。
所以函数模板的特化不能影响函数重载的调用特性,例如,对于Compare函数,如果此时写一个参数为int*类型的函数重载,那么当调用Compare函数实参为两个int*类型的指针时,调用的是函数重载的那个函数,而不是函数模板特化的那个函数。
// Compare函数的重载,当实参传递为Compare(int*, int*)时,优先调用这个函数boolCompare(int*const&a,int*const&b){return*a<*b;}2. 类模板的特化
除了函数模板外,类模板也可以进行特化,类模板的特化本质上也是模板的实例,特化的类模板的内部实现可以和原类模板的实现不相同。类模板分为全特化和偏特化(半特化),下面我们来看类模板的这两种特化的特点。
2.1 类模板的全特化
类模板的全特化即对全部的模板参数都进行了特化处理,类模板的全特化和函数模板的特化方式相似,都需要在定义前面加上template<>以表示为类模板的全特化,而在类名之后需要加上用尖括号括起来的特化的模板参数,特化的模板参数需要和原模版参数一一对应,这其中最难理解的就是参数需要一一对应,下面用例子进行讲解。
如下所示为类的原模版和特化的类模板,其中特化的类模板的参数和原类模板的参数一 一对应,即第一个参数int对应着类型T1,第二个参数int对应着类型T2。
template<classT1,classT2>// 原类模版classA{private:T1 _a1;T2 _a2;};template<>//特化的类模板classA<int,int>{private:T1 _a1;T2 _a2;};2.2 类模板的偏特化
与函数模板不同,类模板并不需要为全部的模板参数提供实参。类模板的特化可以指定部分而非全部的参数,或者指定部分而非全部的参数的特性,而这种特化方式就称为偏特化。类模板的偏特化需要为还没有确定类型的参数保留形参。
注意:只有类模板可以使用偏特化,函数模板不能使用偏特化
类模板的偏特化常见的形式有两种,如下所示:
指定部分参数的类型
当偏特化为指定部分参数的类型时,模板参数中还需要为没有确定类型的参数保留形参,对于原模版来说,参数T1已经确定了类型为int,而参数T2还没有确定类型,所以需要在模板参数中保留参数T2。
template<classT2>//特化的类模板classA<int,T2>{private:T1 _a1;T2 _a2;};指定参数的部分特性
指定参数的部分特性本质上提前特化出参数为指针或者引用类型的参数,当对类A实例化,且参数为两个指针时(以
A<int*, int*> a1;为例),类模板的就会实例化这个版本的特化。这种偏特化的方式需要注意的是,此时T1和T2的类型为int而不是int*。template<classT1,classT2>//特化的类模板classA<T1*,T2*>{private:T1 _a1;T2 _a2;};
2.3 类模板特化的应用-仿函数的改造
在priority_queue的实现中,需要使用默认的仿函数template<class T> class Less;来控制堆为大堆,对于当前版本,如果堆中的数据是指针类型,而此时的需求是要指向的对象最大的指针在堆顶,那使用这样的版本是无法得到正确的堆的,此时就需要创建类模板Less的偏特化版本。
template<classT>classLess// 原类模板/仿函数{public:booloperator()(constT&a,constT&b){returna<b;}};对于目标的偏特化版本,我们需要到达的效果是只要实例化时实参是指针类型,就实例化这个偏特化版本,所以我们就可以通过指定模板参数的特性来实现这个偏特化。类模板的偏特化的代码如下:
template<classT>classLess<T*>// 类模板的偏特化{public:booloperator()(constT*a,constT*b){return*a<*b;}};三. 模板声明和定义不可分离原因
模板声明和定义不可分离原因
在前面STL的学习中我们提到了,模板的声明和定义不可以存在于不同的文件,究其原因,主要还是因为模板没有实例化导致的问题。首先我们要知道,模板的本质其实就是”一个装着代码模板的容器“。模板是按需求实例化的,且模板只有被实例化之后才是真正意义上类或函数。
假设此时一个函数模板的声明和定义分离,声明放在my_template.h的文件中,定义放在my_template.cpp的文件中,如果main.cpp文件中使用了该模板实例化对象,当程序运行时,程序在编译器的控制下会发生如下变化:
- 程序运行时,首先是预处理阶段,预处理阶段会将
main.cpp中包含的头文件my_template.h展开,此时main.cpp文件中就会有模板的声明。预处理完之后,编译器会分别对my_template.cpp和main.cpp文件进行编译 - 在编译
main.cpp文件时,由于文件中只有模板函数的声明而没有定义,此时编译器会留下占位符,等链接时寻找模板函数的地址 - 在编译模板的实现文件
my_template.cpp时,文件中没有对模板实例化的指令,所以编译阶段没有产生模板的实例化的目标代码 - 链接时,
main.cpp没有找到模板函数的地址,此时链接就会报错
问题场景(定义和声明分离)
my_template.h(头文件):只放声明// my_template.htemplate<typenameT>voidmyFunction(T x);// 声明(没有实现)
my_template.cpp(源文件):放定义// my_template.cpp#include"my_template.h"template<typenameT>voidmyFunction(T x){/* 实现 */}// 定义
main.cpp(使用文件):调用模板// main.cpp#include"my_template.h"intmain(){myFunction(42);// 尝试实例化 myFunction<int>return0;}编译和链接过程发生了什么?
步骤 发生了什么 为什么出错 1. 编译 main.cpp- 编译器看到 myFunction(42),知道需要实例化myFunction<int>。 -但它只能看到声明(my_template.h),看不到定义(my_template.cpp中的实现)。 - 因此,编译器无法生成myFunction<int>的目标代码(因为不知道如何实现)。 -结果:main.o中只留下一个“占位符”(符号引用),等待链接器填充地址。编译器需要模板的完整定义才能实例化。声明只告诉编译器“这个函数存在”,但无法生成具体代码。 2. 编译 my_template.cpp- 编译器处理此文件时,没有遇到任何实例化请求(文件内没有调用 myFunction)。 - 因此,它不会为任何类型生成目标代码(模板定义只是“闲置的蓝图”)。 -结果:my_template.o中完全没有myFunction的代码。模板实例化是“按需触发”的。如果定义文件中没有显式调用或显式实例化,编译器会忽略它(避免生成无用代码)。 3. 链接阶段 - 链接器尝试合并 main.o和my_template.o。 -main.o要求提供myFunction<int>的地址,但my_template.o中不存在这个符号。 -结果:链接器报错undefined reference to 'void myFunction<int>(int)'。根本原因:实例化从未发生。定义文件中没有触发实例化的代码,导致目标文件缺失所需符号。
结论:编译器在定义文件(.cpp)中无法主动推断需要哪些实例化,因此不会生成任何目标代码。
解决方案
在模板的定义文件(my_template.cpp)中显式实例化(不推荐)
缺点:对于其它文件中所有不同的实例化模板的方式都需要显式实例化。例如,如果调用了
myFunction(int)就需要在定义文件中写函数模板的int类型的显式实例化,而如果使用myFunction(double),则需要实例化double类型的函数模板……// 显式实例化:告诉编译器“必须为int生成代码”templatevoidmyFunction<int>(int);// 显式实例化:告诉编译器“必须为int生成代码”在声明和定义都放在头文件(my_template.h)中(推荐)
这种解决方案是最简便的,因为只要模板的定义在头文件中,调用处在编译时就可以直接对模板进行实例化,这样就不会导致模板实例化失败。
