目录
摘要:
模版特化
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
📜 [ 声明 ] 由于笔者水平有限,文中难免有疏漏或不妥之处,还望读者不吝赐教