本文参考文章2001 年 7 月的 C/C++ Users Journal,第 19 卷第 7 期:Why Not Specialize Function Templates? 大家有兴趣可以看看原文。
文章目录
- 一、 重载和特化
- 1. 重载
- 2. 特化
- 二、特化和重载的调用优先级
- 1. 第一份代码
- 2. 第二份代码
- 3. 原因
- 三、函数模板特化的书写格式
- 1. 单参的函数模板
- 2. 多参数的函数模板
- ① 全特化:全显式
- ② 全特化:显式 + 推演
- 四、总结:
- 1. 单参数的函数模板
- 2. 多参数的函数模板
首先我们来看一下我们待会儿要接触的主要内容:

一、 重载和特化
1. 重载
正如上面所写的,类不能重载,但函数可以重载,所以类模板没有重载,而函数模板是可以重载的。
//重载的两个函数模板
template<class T>
void sum(T a)
{
}
template<class T1,class T2>
void sum(T1 a, T2 b)
{
}
上面的两个函数模板就是重载的关系。
2. 特化
正如上面所写的,类模板可以全特化,也可以偏特化,而函数模板只能全特化,并不支持偏特化(语言不支持)。
//类模板
template <class T1,class T2>
class Date //基础的类模板
{
};
template <class T1,class T2>
class Date<int,int> //类模板的全特化
{
};
template <class T1>
class Date<T1, int> //类模板的偏特化
{
};
//函数模板
template <class T>
bool Less(T left, T right) //基础的函数模板 —— 为了方便描述,我么称它为 a
{
cout << "Less(T left, T right)" << endl;
return left < right;
}
template<>
bool Less<int>(int left, int right) //函数模板的全特化
{
return left < right;
}
template<class T>
bool Less<T*>(T* lp, T* rp) //函数模板的偏特化,但不支持,会报错
{
return *lp < *rp;
}
虽然函数模板并不支持偏特化,但可以通过重载实现偏特化的效果:
template <class T>
bool Less(T* p1, T* p2) //基础的函数模板 —— 为了方便描述,我么称它为 b
{
cout << "Less(T* p1, T* p2)" << endl;
return *p1 < *p2;
}
这里,b 是 a 的重载,可以实现偏特化指针的效果。
二、特化和重载的调用优先级
1. 第一份代码
我们来看下面一份代码:
//单参的函数模板
// 基本函数模板 a
template <class T>
bool Less(T left, T right)
{
cout << "Less(T left, T right)" << endl;
return left < right;
}
// 基本函数模板 b
template <class T>
bool Less(T* p1, T* p2)
{
cout << "Less(T* p1, T* p2)" << endl;
return *p1 < *p2;
}
// b 的全特化
template<>
bool Less<>(Date* p1, Date* p2)
{
cout << "Less<>(Date* p1, Date* p2)" << endl;
return *p1 < *p2;
}
除此之外,我们还实现了一个 Date 类(日期),可以比较日期的大小,用来测试。
我们运行下面的测试代码:
int main()
{
zzz::Date* p1 = new zzz::Date(2024, 1, 1);
zzz::Date* p2 = new zzz::Date(2025, 1, 1);
cout << Less(p1, p2) << endl;
return 0;
}

发现调用的是: b 的全特化。
2. 第二份代码
下面我们来看第二份代码:
//单参的函数模板
// 基本函数模板 a
template <class T>
bool Less(T left, T right)
{
cout << "Less(T left, T right)" << endl;
return left < right;
}
//a 的全特化
template<>
bool Less<>(Date* p1, Date* p2)
{
cout << "Less<>(Date* p1, Date* p2)" << endl;
return *p1 < *p2;
}
// 基本函数模板 b
template <class T>
bool Less(T* p1, T* p2)
{
cout << "Less(T* p1, T* p2)" << endl;
return *p1 < *p2;
}
看一下测试结果:

调用的是:基本函数模板 b —— 调用的函数和第一份代码有区别了!!!
那么为什么不调用特化版本的函数了,而且特化版本明显更符合调用逻辑?
下面我们来看原因。
3. 原因
我们把两个特化放到一起,对比一下:
//a 的全特化
template<>
bool Less<>(Date* p1, Date* p2)
{
cout << "Less<>(Date* p1, Date* p2)" << endl;
return *p1 < *p2;
}
// b 的全特化
template<>
bool Less<>(Date* p1, Date* p2)
{
cout << "Less<>(Date* p1, Date* p2)" << endl;
return *p1 < *p2;
}
发现,这两个函数写法上是完全一样的,但是却给标注了不同的名字,一个是 “a 的全特化”,另一个是 “b 的全特化”,第一份代码和第二份代码只有这个特化函数的位置不一样,就导致了它成了不同函数模板的特化版本 —— 它会是它前面的一个匹配的基本函数模板的特化版本。所以第一份代码,它在基本函数模板 b 的后面,他就成了 b 的全特化,而第二份代码中,它在它在基本函数模板 a 的后面,他就成了 a 的全特化。
但这也无法解释为什么调用函数发生了变化。下面,我们来看一下原文中是怎么描述调用逻辑的:

- 首先,非模板函数是一等的,在完全匹配的情况下,绝对会调用它,而不是模板函数。
- 其次,在没有合适的非模板函数时,基本函数模板是二等的选择,选择哪个基本函数模板取决于哪个是最匹配的。如果很明显有一个最匹配的基本函数模板,那么就会选定这个。如果该基本模板恰好有专门用于正在使用的类型(匹配的特化版本),则将使用特化版本,否则将使用使用正确类型实例化的基本模板。
所以,选择调用哪个函数是遵循的双类系统,只有 ”非模板函数“ 和 ”基本函数模板“ 两个选择,特化版本的选择是在选定基本函数模板之后才查看的。只有在决定要选择哪个基本模板并且该选择被锁定之后,编译器才会环顾四周,看看是否恰好有合适的模板专用化可用,如果有,该专用化将被使用。
所以,我们就可以解释上面的调用函数的逻辑了:
第一份代码:

第二份代码:

所以,函数模板不让特化,是因为会出这样的问题:即使我们为某种特殊的类型写了一个特化版本,在最匹配的情况下,它也可能不会总是被调用。要想让它在匹配时总是被调用,应该把它写成非模板函数,而不是特化版本。
三、函数模板特化的书写格式
在【一】中,全特化版本是这样写的:
template<>
bool Less<int>(int left, int right) //函数模板的全特化
{
return left < right;
}
在【二】中,全特化版本是这样写的:
template<>
bool Less<>(Date* p1, Date* p2)
{
cout << "Less<>(Date* p1, Date* p2)" << endl;
return *p1 < *p2;
}
可以看到,一个的<> 中有写类型,而另一个却没有,那么这两种有差别吗?这是单个参数的,那多个参数的要怎么写呢?下面我们来看一下。
1. 单参的函数模板
//单参的函数模板
//a
template <class T>
bool Less(T left, T right)
{
cout << "Less(T left, T right)" << endl;
return left < right;
}
//全特化 - 显式 —— a
template<>
bool Less<Date*>(Date* p1, Date* p2)
{
cout << "a:显式" << endl;
return *p1 < *p2;
}
//全特化 - 推演 -- a
template<>
bool Less<>(Date* p1, Date* p2)
{
cout << "Less<>(Date* p1, Date* p2)" << endl;
return *p1 < *p2;
}
其实两个全特化版本是一样的,只是一个 T 的类型我们指明了(全特化 - 显式),而另一个 T 的类型是推演出来的(全特化 - 推演)。两个是同一个函数,所以如果两个都写出来的话,会显示【已定义】的错误:

我们最开始那张图还记得吗?上面有写类模板只能显式实例化,而函数模板可以显式实例化,也可以推演实例化,这里就是在进行推演。
我们上面还写了一个基本函数模板 b ,它的全特化版本我们也可以写成两种形式:
//b
template <class T>
bool Less(T* p1, T* p2)
{
cout << "Less(T* p1, T* p2)" << endl;
return *p1 < *p2;
}
//全特化 - 推演 -- b
template<>
bool Less<>(Date* p1, Date* p2)
{
cout << "Less<>(Date* p1, Date* p2)" << endl;
return *p1 < *p2;
}
//全特化 - 显式-- b
template<>
bool Less<Date>(Date* p1, Date* p2)
{
cout << "b:显式" << endl;
return *p1 < *p2;
}
这里,有一个很有意思的点,不知道大家注意到没有:b 的【全特化 - 显式】和 a 的【全特化 - 显式】是不一样的:

这也是和 a、b 的基本函数模板格式是对应的:

而前面我们也说过了,【全特化 - 显式】和【全特化 - 推演】两个函数是一样的,所以我们可以知道推演的类型:一个是 Date,另一个是 Date*:

所以前面说的 a、b 两个的全特化推演版本写法一样,也仅仅是写法是一样,其实推演出来的类型是不一样的。
2. 多参数的函数模板
下面我们来看一个多参数的基本函数模板和特化版本,看它的特化版本可以怎样写:
//a
template <class T1, class T2>
void sum(T1 a, T2 b)
{
cout << "sum(T1 a, T2 b)" << endl;
}
// 全特化:全推演 - a
template<>
void sum<>(int a, int b)
{
cout << " sum<>(int a, int b)" << endl;
}
//全特化:全显式 - a
template <>
void sum<int, char>(int a, char b) // use <int,char>(char,int> has error —— 必须和参数类型对应
{ // use <int*,char*>(int a, char b) has error
// use <int ,char> (int* a, char* b) has error
cout << " sum<int, char>(int a, char b) " << endl;
}
//全特化:(显式 + 推演) - a
template <>
void sum<char>(char a, int b)
{
cout << " sum<char>(char a, int b) " << endl;
}
//全特化:(显式 + 推演) - a
template <>
void sum<int>(int a, char b)
{
cout << " sum<int>(int a, char b) " << endl;
}
//全特化:(推演 + 显式) - a // error —— 如果推演的化,只能从左向右推演类型
template <>
void sum<char>(int a, char b)
{
cout << " sum<char>(int a, char b) " << endl;
}
这里面需要注意的点,注释上有说明。下面我们来具体看一下。
① 全特化:全显式
这里要说明的是,显式写出的模板类型,一定要和参数类型对应。

② 全特化:显式 + 推演
这里要说明的是,如果推演的化,只能从左向右推演类型,跟给缺省值是一样的道理。

四、总结:
1. 单参数的函数模板

2. 多参数的函数模板


本文到这里就结束了,如果对您有帮助,希望得到您的一个赞!🌷
如果有哪里有问题,希望大家评论指出或跟我讨论,感谢大家!
祝大家每天进步!😄

915

被折叠的 条评论
为什么被折叠?



