函数模板进阶 - 为什么函数模板不要特化?

本文参考文章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 T1class T2>
class Date	    	//基础的类模板
{
};

template <class T1class 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;
}

这里,ba 的重载,可以实现偏特化指针的效果。

二、特化和重载的调用优先级

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 的【全特化 - 显式】是不一样的:
在这里插入图片描述
这也是和 ab 的基本函数模板格式是对应的:
在这里插入图片描述
而前面我们也说过了,【全特化 - 显式】和【全特化 - 推演】两个函数是一样的,所以我们可以知道推演的类型:一个是 Date,另一个是 Date*
在这里插入图片描述
所以前面说的 ab 两个的全特化推演版本写法一样,也仅仅是写法是一样,其实推演出来的类型是不一样的。

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. 多参数的函数模板

在这里插入图片描述
在这里插入图片描述


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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值