news 2026/9/29 2:22:12

模版的特化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
模版的特化

目录

摘要:

模版特化

1:函数模板特化

2:类模版特化

①:类模版特化时的成员函数参数类型

②:全特化

③:偏特化

3:tyename的用途


摘要:

本文记录了 C++ 模板特化的学习要点:函数模板特化、类模板特化(含成员函数参数类型、全特化、偏特化),以及 typename 在模板中的用途。


模版特化

概念:针对于模版接收到的某些类型进行特殊处理,就叫做模版特化

通常情况下,使用模板可以实现泛型编程,但代码可能对某些类型无法进行正确处理,所以对于这些类型需要特殊处理~

比如我们可以实现一个模版函数,去泛型比较int,double变量的大小,但是如果传递过来的是一个指针类型呢?此时就会直接比较地址的大小,但这不是我们期望的比较方式,我们期望比较的是指针指向的内容的大小!所以此时我们需要对模版参数接收到指针类型这一情况,进行特殊处理!

模板无法处理指针类型例子:

#include<iostream> using namespace std; // 函数模板 -- 参数匹配 template<class T> bool Less(T left, T right) { return left < right; } int main() { cout << Less(1, 2) << endl; // 可以比较,结果正确 cout << Less(1.4, 2.7) << endl; // 可以比较,结果正确 int a = 10; int b = 20; int* p1 = &a; int* p2 = &b; cout << Less(p1, p2) << endl; // 可以比较,结果错误 return 0; }

解释:

传递的p1指针指向的内容为10,p2指针指向的内容为20,但是Less函数返回的0,表示p1地址大于p2地址,而不是比较的内容!所以我们需要进行函数模版的特化处理!


1:函数模板特化

// 函数模板 -- 参数匹配 template<class T> bool Less(T left, T right) { return left < right; } // 对Less函数模板进行特化 template<> bool Less<int*>(int* left, int* right) { return *left < *right; } int main() { cout << Less(1, 2) << endl; // 可以比较,结果正确 cout << Less(1.4, 2.7) << endl; // 可以比较,结果正确 int a = 10; int b = 20; int* p1 = &a; int* p2 = &b; cout << Less(p1, p2) << endl; // 可以比较,结果错误 return 0; }

解释:

①:注意写法!特化部分第一行template<>,第二行bool Less<int*>(int* left, int* right),这两行缺一不可!其中的int*就是代表对原函数模版参数接收到的int*类型进行特殊处理

②:template<>是空的括号,第二行bool Less后面的<int*>中带有特殊处理的类型

③:这样一来,当我们传递的参数为int*时,就会直接匹配到我们写的函数模版特化版本


❓️:为啥一定要对这个函数模版进行特化处理,我直接重写一个参数为int*的Less函数不也可以吗?

💡:是的,当然可以!

如下:

// 函数模板 -- 参数匹配 template<class T> bool Less(T left, T right) { return left < right; } // 重写一个Less函数,构成重载 bool Less(int* left, int* right) { return *left < *right; } int main() { cout << Less(1, 2) << endl; // 可以比较,结果正确 cout << Less(1.4, 2.7) << endl; // 可以比较,结果正确 int a = 10; int b = 20; int* p1 = &a; int* p2 = &b; cout << Less(p1, p2) << endl; // 可以比较,结果错误 return 0; }

解释:

①:这里两个Less构成重载,当传递的类型为int*时,会走现成的第二个Less函数,而不是调用模版函数,编译器原则就是这么轻松怎么来

②:并且也更推荐新写一个函数构成重载的做法,更优秀,毕竟模版特化不是针对函数模版产生的,而是类模版!

💡:虽然模版特化不是针对函数模版产生的,但是讲解函数模版的特化,有助于我们理解另一个问题,那就是"模版的特化引发的权限扩大"问题!要学会怎么处理该问题!

和上面同样的例子,但是我们把Less参数前加上const修饰:

// 函数模板 -- 参数匹配 template<class T> bool Less(const T left, const T right) { return left < right; }

所以我们自然而然会觉得函数模版特化应该下面这么写:

// 对Less函数模板进行特化 template<> bool Less<int*>(const int* left, const int* right) { return *left < *right; }

但此时报错:

解释:

报错原因:我们写的模版特化,不是上面那个模版函数的特化!因为检查到参数类型不匹配!


❓️:我们虽然傻瓜式替换,但是非常严谨的把每个T都换成int*,为什么还会不匹配呢?

💡:因为const T中const是修饰T这个整体的,T无法被修改,我们想写T为int*的特化,那么也应该保证int*无法被修改,也就是指针本身不能被修改,而const int* 中const保证的不是int*这个指针无法被修改,而是指针指向的内容不能被修改!所以二者类型不同!导致报错!

正确特化代码:

// 函数模板 -- 参数匹配 template<class T> bool Less(const T left, const T right) { return left < right; } // 对Less函数模板进行特化 template<> bool Less<int*>(int* const left, int* const right) { return *left < *right; } int main() { cout << Less(1, 2) << endl; // 可以比较,结果正确 cout << Less(1.4, 2.7) << endl; // 可以比较,结果正确 int a = 10; int b = 20; int* p1 = &a; int* p2 = &b; cout << Less(p1, p2) << endl; // 可以比较,结果错误 return 0; }

解释:

①:正确写法,int* const,此时的const才是修饰的指针本身!此时特化函数的参数类型和原模版函数的参数类型才匹配,才不会报错!

②:在某些地方,也称此处的错误为,"模版特化造成的权限放大",但这句话不够完善!

首先const T 到const int*的确是权限放大,因为前者T不可修改,但是后者int*这个指针可以修改,所以权限放大了,但不是因为权限放大导致报错,而是权限放大导致类型不同,从而和原模版函数的参数类型不匹配(const T 和 const int*类型不匹配),导致编译器报错认为你写的函数模版的特化和你的函数模版不匹配!这才是正解~

③:但需要知道,const int*和int* const其实只是能够修改的东西不同

类型指针本身所指对象
int* const&❌ 不可改✅ 可改
const int*&✅ 可改❌ 不可改

单独讨论const int*和int* const其实不涉及到权限放大,只能说const所保护的东西不同!但是在模版特化这里,它关心的是仅仅T的权限,也就是只在乎T是否被保护起来了?原模版函数的T被const修饰不可被修改,那么你特化中的int*也必须不能被修改,但是const int*能够修改int*,所以才成为T的权限被放大了!


在库中,一个模版函数的接口参数往往是这样写的,不仅有const,还有引用&

template<class T> bool Less(const T& left, const T& right) { return left < right; }

解释:const T&这种写法的初心,是利用其中的&方便接收具有资源的类型,避免重复拷贝,但单独加上&,导致无法接收1,10,2.5这种常量(本质是权限放大),所以再用const修饰T得到const T&,避免权限放大;这也是库中大多数参数设计这样的原因:既能接受具有资源类型,也能接受常量,并且不能对参数进行修改,确保程序的健全性


同理,对于这种模版函数的int*特化,也会出现上文中的问题,我们依旧把const放在*的后面即可

// 函数模板 -- 参数匹配 template<class T> bool Less(const T& left, const T& right) { return left < right; } // 对Less函数模板进行特化 template<> bool Less<int*>(int* const& left, int* const& right) { return *left < *right; }

解释:正确理解int* const&

从右往左读(C++ 声明阅读法)

符号含义
&这是一个引用
const被引用的对象是 被const修饰的
int*被引用对象的类型是int*(指针)

合起来:"一个指向int*的引用,且这个int*是 const 的"

也就是:引用绑定到一个"常指针"上(指针本身不可改)。

它不是const int*,也不是const int*&,编译器分得清清楚楚,不用担心被误解。


2:类模版特化

模版特化可以说就是为了类模版而产生的!函数模版的特化可以用函数重载代替,但是类模版的特化无法被替代!此外不管是函数模版特化还是类模板特化,都是因为对模版参数接收到的某些类型无法进行正确处理,在下面我们就不再举例一个无法处理某些类型的成员函数了,直接进行特化!

①:类模版特化时的成员函数参数类型

我们在函数模版特化时的问题代码如下:

// 函数模板 -- 参数匹配 template<class T> bool Less(const T left, const T right) { return left < right; } // 对Less函数模板进行特化 template<> bool Less<int*>(const int* left, const int* right) { return *left < *right; } int main() { int a = 10; int b = 20; int* p1 = &a; int* p2 = &b; cout << Less(p1, p2); return 0; }

而当我们将其变成类时,如下:

// 模板类 template<class T> class A { public: bool Less(const T left, const T right) { return left < right; } }; // 模板类特化 template<> class A<int*> { public: bool Less(const int* left, const int* right) { return *left < *right; } }; int main() { int a = 10; int b = 20; int* p1 = &a; int* p2 = &b; A<int*> aa; cout << (aa.Less(p1, p2)); return 0; }

解释:

①:我们并没有把const放在*的后面,但是程序编译通过,且函数返回结果正确?!

②:这是因为"类模板特化"这个行为的主体是"类",而不是其中的某个函数,所以没有要求类中的成员函数的参数类型完全匹配

③:但是,仍然强烈建议,成员函数的类型也匹配,因为对模板类的特化本来就应该改变成员函数的参数类型,尽管它允许,但我们也不该这么做,应该维护代码的严谨性!

所以还是建议下面这么写:

// 模板类特化 template<> class A<int*> { public: bool Less(int* const left, int* const right) { return *left < *right; } };


而类模版的特化又分为全特化和偏特化

②:全特化

全特化:即特化的时候,将模板参数列表中所有的参数都确定具体的类型!
//模板类 template<class T1, class T2> class Data { public: Data() { cout << "Data<T1, T2>" << endl; } private: T1 _d1; T2 _d2; }; //类模板全特化 template<> class Data<int*, int*> { public: Data() { cout << "Data<int*, int*>" << endl; } private: int _d1; char _d2; }; int main() { Data<int, int> d1; Data<int*, int*> d2; return 0; }

解释:写法和函数模板的特化又有些许不同

特化部分第一行仍然是template<>,但第二行变成了class Data<int*, int*>,而函数模板特化第二行是bool Less<int*>(int* left, int* right),区别就在于函数模板特化有参数部分,而类模板全特化没有,去掉参数部分,一个是class Data<int*, int*>,一个是bool Less<int*>,都是类似的,前者多一个int*是因为我们举例全特化,当然至少要举例两个模板参数的类


③:偏特化

偏特化:

a:特化的时候,只将模板参数列表中部分参数确定具体类型,另一部分还是模版T

b:特化的时候,所有的模版参数都规定了类型,但并不具体,都和T又换

a例子:

偏特化,对class Data<T1, int>进行处理

template<class T1, class T2> class Data { public: Data() { cout << "Data<T1, T2>" << endl; } private: T1 _d1; T2 _d2; }; // 将第二个参数特化为int template <class T1> class Data<T1, int> { public: Data() { cout << "Data<T1, int>" << endl; } private: T1 _d1; int _d2; }; int main() { Data<int, int> d1; Data<int, char> d2; return 0; }

解释:

①:Data<int, int> d1;调用的是偏特化,因为第二个int正好符合偏特化的第二个参数int,对于编译器来说,如果走偏特化都只用传递一个模版参数了,何乐而不为?

②:Data<int, char> d2;调用的是模板类本身,因为第二char和偏特化的int不符合,只能走模板类

③:所以要想走偏特化,那么偏特化的所有参数类型都必须满足!

Data<int, int> d1;能调用偏特化,1是因为int符合直接偷懒,2是因为另一个int也能传递给偏特化的T1

Data<int, char> d2;不能调用偏特化,因为char不匹配,直接不会走偏特化,不管另一个能否偷懒


b例子:

特殊的偏特化

//基础模板类 template<class T1, class T2> class Data { public: Data() { cout << "Data<T1, T2>" << endl; } private: T1 _d1; T2 _d2; }; //两个参数偏特化为指针类型 template <typename T1, typename T2> class Data <T1*, T2*> { public: Data() { cout << "Data<T1*, T2*>" << endl; } private: T1 _d1; T2 _d2; }; //两个参数偏特化为引用类型 template <typename T1, typename T2> class Data <T1&, T2&> { public: Data(const T1& d1, const T2& d2) : _d1(d1) , _d2(d2) { cout << "Data<T1&, T2&>" << endl; } private: const T1& _d1; const T2& _d2; }; int main() { Data<int, int> d1;//走模板类 Data<int, char> d2;//走模板类 cout << endl; Data<int*, char> d3;//走模板类 Data<int, char*> d4;//走模板类 cout << endl; Data<int*, char*> d5;//走指针偏特化 cout << endl; Data<int&, double&> d6(1, 2.0);//走引用偏特化 cout << endl; return 0; }

解释:

①:这也叫偏特化,不要误认为两个模版都确定为了指针/引用类型就是全特化,全特化指的是确定为了int* int&这种具体的类型,而这里是T*/T&,不是具体的类型,所以仍然是偏特化

②:d3会走模板类就是因为char类型不匹配指针偏特化的参数类型;同理,d4的int不匹配偏特化的参数类型;而d5会走指针偏特化,因为两个类型是指针类型,才匹配上了指针偏特化

③:d6会走引用偏特化,因为两个类型都是引用类型,才匹配上了指针偏特化

④:所以如果我们Data<int, double&> d7;那么会匹配到模板类本身,int给T1,double&给T2,但是引用类型的值必须初始化,这里会报错引用未初始化,而不是类型不匹配~


注意:

①:模版参数T可以接收int类型,double类型,同理也可以直接接收int*类型 int&类型,因为这些都是类型!甚至还可以接收类类型!但是不能传递确切的值,比如1,1.0,&a,这不叫类型!

②:偏特化的参数必须同时满足要求,一个都不能少。"传一半参数、想蹭偏特化"是不成立的,因为偏特化模板不认半匹配,一个位置不满足,整个偏特化就不命中,直接落回主模板。想要"一个满足、一个不满足"的特殊处理,就老老实实为这种形状单独定义一个偏特化,而不是投机取巧传一半去蹭别人的。要么完整满足,要么别特化,要么专门为你的形状写新偏特化。这才是优秀做法。

❓️:你在第①点中说模版参数T不能接收具体的值,但是在函数模版中不是直接传递给T值吗?

💡:如下代码:

template<class T> void test(T val) // val 是函数参数,它接收"值" { cout << val; } int main() { test(10); // (10)传给 val;T 同时被推导为 int(类型) }

解释:

①:看起来是将值10直接传给了本该接收int类型的T,但是其实内部的转化编译器做了,10传给T,编译器推导出T为int,并且把10赋给了val

②:如果这里的函数参数为T & val,那么我们就不能传递10了,应该传递值为10的变量a,此时依旧可以理解为编译器推导出了a的类型为int,a的值10赋给了val!


💡:所以现在我们可以理解这句话了!"类模版的特化无法被替代"

例子代码:

//模板类 template<class T1, class T2> class Data { public: Data() { cout << "Data<T1, T2>" << endl; } private: T1 _d1; T2 _d2; }; //类模板全特化 template<> class Data<int*, int*> { public: Data() { cout << "Data<int*, int*>" << endl; } private: int _d1; char _d2; }; int main() { Data<int, int> d1; Data<int*, int*> d2; return 0; }

你怎么也无法做到新创建一个类,来替换掉这个特化!即使你创建一个非模板类new_Date类,把其中的成员变量全部改成int*类型的也不行!

因为:

类模板:Data<int,int>、Data<int*,int*>—— 只能显式指定模板参数,编译器永远先看模板,没法像函数那样"自动匹配到一个普通类"。

函数模板:调用时Less(2,3)、Less(p1,p2)—— 函数实参和模板参数一起传,编译器自动推导,所以能匹配到"普通函数重载"。

所以这就是为什么"类模版的特化无法被替代"


3:tyename的用途

以前我们说模版参数中使用class和typename的效果是一样的,但在某些地方,必须使用typename,今天就来讲讲什么地方必须使用typename!

正确代码:

#include <iostream> #include <vector> #include <list> using namespace std; void test1(vector<int>& v) { vector<int>::iterator it = v.begin(); // ✅ 直接取,不用 typename cout << "test1: " << *it << endl; } int main() { vector<int> v = { 10, 20, 30 }; test1(v); // ✅ 确切类型 return 0; }

解释:

为什么test这个函数内部,能够直接访问vector类的v对象的迭代器呢?因为test函数的模板参数已经确切了,接收到的参数就是一个vector的对象!所以函数内部允许去取类内的迭代器!


错误代码:

#include <iostream> #include <vector> #include <list> using namespace std; // ============================================================ // 情况②:参数是"模板参数" T → 取 T::iterator 必须加 typename // ============================================================ template<class T> void test2(T& v) { T::iterator it = v.begin(); // ❌ 不加 typename 会报错 //typename T::iterator it = v.begin(); // ✅ 加 typename 才能过 cout << "test2: " << *it << endl; } int main() { vector<int> v = { 10, 20, 30 }; test2(v); // ✅ 确切类型 return 0; }

解释:

①:对于这个模版函数来说,即使main中已经给你的T传递了vector对象,但是函数内部依旧不能通过T去取vector对象中的迭代器,因为在编译的时候,这个模版函数的参数T还未确定是什么,所以编译器不知道 "T::iterator"意味着什么,是取T类中的静态成员(具体变量)?还是取T类中的某一类型,所以报错!

②:而我们要取的是迭代器类型(是一种类型),从而通过迭代器类型去创建it,所以我们要在此行代码前加上"typename",意味着告诉编译器,要取的是一个类型,此处语法没问题!等到模版参数T接收到确切类型后,你就去取类型,创建对应变量it!

当然讲这个例子的目的在于介绍typename的用途,此处代码最简单的方式直接用auto即可!

#include <iostream> #include <vector> #include <list> using namespace std; //// ============================================================ //// 情况③:用 auto → 最省事,不用管 typename //// ============================================================ template<class T> void test3(T& v) { auto it = v.begin(); // ✅ 完全不用 typename cout << "test3: " << *it << endl; } int main() { vector<int> v = { 10, 20, 30 }; test3(v); // ✅ 确切类型 return 0; }

📌 [ 作者 ] shylyly
📃 [ 首次发布 ] 2025.3.24
❌ [ 最新修改 ] 2026.9.28
📜 [ 声明 ] 由于笔者水平有限,文中难免有疏漏或不妥之处,还望读者不吝赐教

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 2:22:05

【GitHub项目实战】FastGPT 搭建本地 DeepSeek R1 服务与专属知识库

在这个快速发展的AI时代,如何高效搭建本地智能系统成为了许多开发者和企业关注的焦点。为了帮助轻松搭建一个强大的本地AI服务详细介绍如何通过一键部署 LM Studio + DeepSeek R1,并搭建 AI 专属知识库。 通过这个过程可以实现对自定义数据的快速处理、无缝集成以及数据安全…

作者头像 李华
网站建设 2026/9/29 2:20:35

I3C高速总线解析:RK3576 DTS配置与调试实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 2:20:08

Moivepy批量创建自定义字幕

在视频内容制作中,字幕不仅帮助观众理解视频内容,也增加了视频的专业性和可访问性。手动添加字幕往往费时费力,而借助Python的MoviePy库,可以实现自动化字幕处理,批量添加标准化、个性化的字幕,大大提升了视频后期制作的效率。 本文将详细介绍如何配置环境、操作字幕文件…

作者头像 李华
网站建设 2026/9/29 2:20:07

MoviePy批量添加固定和随机移动水印

视频版权保护在当今数字内容领域变得尤为重要。通过水印添加技术,不仅可以有效标识原创,还能提高视频的安全性。MoviePy是一个强大而灵活的Python库,能够轻松实现视频编辑、特效添加等多种操作。 本文介绍如何通过MoviePy批量为视频添加水印,涵盖固定水印和随机移动水印的…

作者头像 李华
网站建设 2026/9/29 2:19:24

51单片机实现智能花盆灌溉系统

1. 项目概述&#xff1a;为什么一个51单片机就能管好一盆花&#xff1f;你可能在淘宝上见过那种带LED屏、能连手机App的“智能花盆”&#xff0c;价格动辄两三百。但真正拆开看&#xff0c;里面跑的很可能就是一块不到五块钱的STC89C52RC——没错&#xff0c;就是那个被高校电子…

作者头像 李华