C++模板进阶:非类型模板参数、模板特化与分离编译一次搞懂

发布时间:2026/10/9 10:19:57
C++模板进阶:非类型模板参数、模板特化与分离编译一次搞懂 C模板进阶非类型模板参数、模板特化与分离编译一次搞懂文章目录C模板进阶非类型模板参数、模板特化与分离编译一次搞懂一、非类型模板参数1 类型模板参数2 非类型模板参数3 非类型模板参数的特点必须是编译期能够确定的值浮点数、类对象以及字符串不能作为非类型模板参数二、模板特化1 为什么需要模板特化三、函数模板特化1 函数模板特化的基本形式第一第二第三第四2 函数模板特化之后发生了什么3 为什么函数模板一般不推荐特化四、类模板特化五、类模板全特化1 什么是全特化2 全特化的调用六、类模板偏特化1 什么是偏特化七、偏特化形式一部分参数特化八、偏特化形式二进一步限制参数类型1 指针类型偏特化九、引用类型偏特化十、类模板特化的匹配Datadouble, intDataint, doubleDataint*, int*Dataint, int十一、模板特化在排序中的应用排序普通对象十二、排序指针时的问题十三、通过类模板特化解决指针排序问题十四、模板分离编译什么是分离编译十五、模板为什么容易出现分离编译问题十六、为什么模板分离编译容易出问题十七、解决模板分离编译的方法方法一声明和定义放在同一个文件方法二显式实例化十八、模板的优点1 代码复用2 灵活性高3 STL 的基础十九、模板的缺点1 可能造成代码膨胀2 编译时间可能变长3 模板错误信息复杂模板特化与分离编译的核心关系一、非类型模板参数模板参数主要分为两类类型模板参数非类型模板参数1 类型模板参数类型模板参数就是我们平时最常见的写法templateclassTclassStack{// ...};这里的T就是类型模板参数使用时可以传入不同的类型Stackints1;Stackdoubles2;Stackchars3;T最终会被替换成对应的类型2 非类型模板参数非类型模板参数不是传入一个类型而是传入一个编译期能够确定的常量例如templateclassT,size_t N10classArray{public:Toperator[](size_t index){return_array[index];}constToperator[](size_t index)const{return_array[index];}size_tsize()const{return_size;}boolempty()const{return_size0;}private:T _array[N];size_t _size;};这里classT是类型模板参数而size_t N是非类型模板参数因此Arrayint,10a1;Arraydouble,20a2;其中int和double是类型参数10和20是非类型参数3 非类型模板参数的特点非类型模板参数有两个非常重要的特点必须是编译期能够确定的值例如Arrayint,10a;这里的10在编译阶段就已经确定不能使用运行过程中才能确定的普通变量作为模板参数浮点数、类对象以及字符串不能作为非类型模板参数例如templatedoubleNclassA{};这种形式是不允许的因此理解非类型模板参数时可以简单记住类型模板参数传类型非类型模板参数传编译期常量二、模板特化1 为什么需要模板特化模板最大的作用就是让代码能够适用于多种类型例如templateclassTboolLess(T left,T right){returnleftright;}对于普通类型来说没有问题Less(1,2);Less(1.0,2.0);因为整数和浮点数都可以直接使用进行比较但是遇到指针就可能出现问题例如Dated1(2022,7,7);Dated2(2022,7,8);Date*p1d1;Date*p2d2;Less(p1,p2);此时模板推导出TDate*于是模板实际上变成类似boolLess(Date*left,Date*right){returnleftright;}这里比较的不是两个日期对象而是两个指针保存的地址也就是说leftright比较的是地址大小而我们真正希望比较的是*left*right也就是两个指针所指向的日期对象这时候就需要模板特化三、函数模板特化1 函数模板特化的基本形式首先必须已经存在一个基础函数模板templateclassTboolLess(T left,T right){returnleftright;}然后针对特殊类型进行特化templateboolLessDate*(Date*left,Date*right){return*left*right;}这里需要注意几个地方第一前面必须有基础模板templateclassTboolLess(T left,T right)第二特化时写template尖括号里面为空第三函数名后面指定特化的类型LessDate*第四函数参数列表需要与基础模板对应的参数类型保持一致Date*left,Date*right2 函数模板特化之后发生了什么普通类型仍然使用普通模板Less(1,2);相当于使用templateclassTboolLess(T left,T right){returnleftright;}而Less(p1,p2);由于参数类型是Date*会匹配我们专门针对Date*写的特化版本templateboolLessDate*(Date*left,Date*right){return*left*right;}因此最终比较的是*left*right而不是leftright3 为什么函数模板一般不推荐特化函数模板特化虽然可以解决特殊类型的问题但是实际使用中通常还有一种更简单的方式直接重新定义一个普通函数例如boolLess(Date*left,Date*right){return*left*right;}这样代码更加直观因此函数模板如果遇到不能处理或者处理有误的类型为了实现简单通常直接给出一个普通函数函数模板特化涉及的参数类型越复杂代码越容易变得难以理解所以实际开发中函数模板通常不建议使用特化四、类模板特化类模板同样可以进行特化类模板特化主要分为全特化偏特化五、类模板全特化1 什么是全特化全特化就是把模板参数列表中的所有参数都确定下来例如基础模板templateclassT1,classT2classData{public:Data(){coutDataT1, T2endl;}private:T1 _d1;T2 _d2;};这个模板有两个类型参数T1 T2现在针对intchar进行完全特殊化templateclassDataint,char{public:Data(){coutDataint, charendl;}private:int_d1;char_d2;};这就是全特化因为T1 T2全部被确定了2 全特化的调用Dataint,intd1;Dataint,chard2;对于Dataint,int没有对应的全特化版本因此使用基础模板对于Dataint,char存在对应的全特化版本因此使用Dataint,char这个特化版本可以理解成基础模板负责通用情况全特化负责某一种完全确定的特殊情况六、类模板偏特化1 什么是偏特化偏特化并不只是简单地理解成只特化一部分参数更准确的理解是针对模板参数进一步进行条件限制从而设计出一个更加特殊的版本例如基础模板templateclassT1,classT2classData{public:Data(){coutDataT1, T2endl;}private:T1 _d1;T2 _d2;};七、偏特化形式一部分参数特化例如只把第二个参数限制为inttemplateclassT1classDataT1,int{public:Data(){coutDataT1, intendl;}private:T1 _d1;int_d2;};这里T1仍然是未知类型但是T2已经被限制为int所以它属于偏特化例如Datadouble,intd1;符合DataT1,int因此使用偏特化版本而Dataint,doubled2;不符合这个条件因此使用基础模板八、偏特化形式二进一步限制参数类型偏特化还可以进一步限制模板参数必须满足某种形式例如把两个参数都限制成指针templatetypenameT1,typenameT2classDataT1*,T2*{public:Data(){coutDataT1*, T2*endl;}private:T1 _d1;T2 _d2;};此时Dataint*,double*d;就符合这个偏特化版本因为T1intT2double最终形成Dataint*,double*1 指针类型偏特化templatetypenameT1,typenameT2classDataT1*,T2*{};它限制的是第一个参数必须是指针 第二个参数必须是指针例如Dataint*,double*d1;可以匹配DataT1*,T2*但是Dataint,doubled2;不能匹配九、引用类型偏特化同样可以针对引用类型进行偏特化templatetypenameT1,typenameT2classDataT1,T2{public:Data(constT1d1,constT2d2):_d1(d1),_d2(d2){coutDataT1, T2endl;}private:constT1_d1;constT2_d2;};这个版本要求两个模板参数都是引用类型例如Dataint,intd4(1,2);就可以匹配这个偏特化版本十、类模板特化的匹配通过下面几个例子可以理解不同版本之间的匹配关系Datadouble,intd1;Dataint,doubled2;Dataint*,int*d3;Dataint,intd4(1,2);Datadouble, int匹配DataT1,int所以使用第二个参数为int的偏特化版本Dataint, double不满足前面的特殊条件所以使用基础模板Dataint*, int*满足DataT1*,T2*所以使用指针偏特化版本Dataint, int满足DataT1,T2所以使用引用偏特化版本因此理解类模板偏特化的关键就是看当前模板参数是否满足某个特化版本的条件十一、模板特化在排序中的应用模板特化一个非常典型的应用场景就是排序例如定义一个比较器templateclassTstructLess{booloperator()(constTx,constTy)const{returnxy;}};这个类模板本质上就是一个函数对象使用方式LessDate()(d1,d2);内部最终执行d1d2排序普通对象例如vectorDatev1;v1.push_back(d1);v1.push_back(d2);v1.push_back(d3);sort(v1.begin(),v1.end(),LessDate());这里v1存放的是Date对象因此LessDate中的returnxy;比较的是两个Date对象如果Date已经实现了operator就可以正常按照日期进行排序十二、排序指针时的问题现在如果容器中存放的是指针vectorDate*v2;v2.push_back(d1);v2.push_back(d2);v2.push_back(d3);然后直接sort(v2.begin(),v2.end(),LessDate*());此时模板中的T是Date*因此比较器相当于structLessDate*{booloperator()(Date*x,Date*y)const{returnxy;}};这里比较的是xy也就是两个指针的地址而不是指针指向的日期对象因此排序结果可能不是按照日期升序排列十三、通过类模板特化解决指针排序问题针对Date*进行特化templatestructLessDate*{booloperator()(Date*x,Date*y)const{return*x*y;}};注意这里发生了一个非常重要的变化普通版本returnxy;比较的是指针特化版本return*x*y;比较的是指针指向的对象这样sort(v2.begin(),v2.end(),LessDate*());在排序过程中就会按照Date对象本身进行比较这就是类模板特化非常典型的应用十四、模板分离编译什么是分离编译一个完整的 C 项目通常由多个源文件组成例如a.h a.cpp main.cpp每个源文件先单独编译生成目标文件最后再把多个目标文件链接起来形成可执行程序这种方式称为分离编译模式十五、模板为什么容易出现分离编译问题普通函数经常采用// a.hintAdd(intleft,intright);然后// a.cppintAdd(intleft,intright){returnleftright;}最后// main.cpp#includea.hintmain(){Add(1,2);}这是正常的但是模板不一样例如// a.htemplateclassTTAdd(constTleft,constTright);然后把定义放到// a.cpptemplateclassTTAdd(constTleft,constTright){returnleftright;}最后// main.cpp#includea.hintmain(){Add(1,2);Add(1.0,2.0);}这种写法可能导致链接阶段出现问题十六、为什么模板分离编译容易出问题模板不是普通函数编译器在使用模板时需要根据实际传入的类型生成对应的实例例如Add(1,2);需要生成类似Addint而Add(1.0,2.0);需要生成类似Adddouble如果当前编译单元只有模板声明却看不到模板的完整定义编译器就无法正常完成对应的实例化过程所以模板通常不适合采用传统的.h 放声明 .cpp 放定义这种方式进行分离十七、解决模板分离编译的方法方法一声明和定义放在同一个文件这是最常用的方法例如直接放到xxx.hpp中templateclassTTAdd(constTleft,constTright){returnleftright;}然后使用时#includexxx.hpp这样使用模板的时候编译器能够看到完整的模板定义因此可以根据实际类型完成实例化这种方法也比较推荐方法二显式实例化另一种方式是在模板定义的位置显式实例化例如可以针对确定的类型进行实例化但是这种方式需要提前知道需要哪些类型模板本身的灵活性会受到影响因此实际使用中一般不推荐这种方式十八、模板的优点模板最大的价值就是代码复用例如一个模板函数可以适用于多种类型templateclassTTAdd(constTleft,constTright){returnleftright;}不需要分别编写intAdd(int,int);doubleAdd(double,double);floatAdd(float,float);而是通过一个模板统一描述因此模板具有以下特点1 代码复用减少重复代码2 灵活性高同一套代码可以适配不同类型3 STL 的基础C 标准模板库 STL 大量使用模板实现例如vector list map set等容器都建立在模板机制之上十九、模板的缺点模板虽然强大但也存在明显缺点1 可能造成代码膨胀不同类型使用同一个模板时编译器可能生成不同的实例例如AddintAdddoubleAddfloat这些都可能产生对应的代码当模板使用非常广泛时可能造成代码体积增加2 编译时间可能变长模板需要在编译过程中进行实例化项目规模较大时大量模板代码可能增加编译时间3 模板错误信息复杂模板出现编译错误时编译器可能输出非常长的错误信息尤其是 STL 和多层模板嵌套时错误信息往往不容易直接定位因此使用模板时需要重点关注模板参数是否正确 模板实例化是否符合预期 是否存在对应的特化版本模板特化与分离编译的核心关系整个模板进阶中最重要的几个关系可以归纳为非类型模板参数 ↓ 模板参数不仅可以是类型也可以是编译期常量 模板特化 ↓ 通用模板无法正确处理某些特殊类型 ↓ 针对特殊情况提供特殊实现 函数模板特化 ↓ 可以针对特定类型提供特殊函数实现 ↓ 实际开发中通常直接写普通函数更简单 类模板特化 ↓ 全特化 偏特化 ↓ 可以针对特定类型或特定类型组合提供特殊实现 模板分离编译 ↓ 模板实例化需要看到完整定义 ↓ 通常将模板声明和定义放在同一个 .hpp 文件中理解这些内容后模板进阶的核心逻辑就是模板负责通用代码特化负责特殊情况非类型参数负责把编译期常量传给模板而模板通常需要让编译器在使用时能够看到完整定义

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询