C++之 模板(下)模板的特化、类型萃取、分离编译

在上篇博客中我们简单讲了一下两大部分:函数模板类模板,本篇博客我们讨论一下它的边缘知识~


非类型模板参数

模板参数分为:类型形参非类型形参

  • 类型形参:出现在模板参数列表中,跟在class或者typename之后的参数类型名称。
  • 非类型形参,就是用一个常量作为类 / 函数模板的一个参数,在类 / 函数模板中可将该参数当成常量来使用。

仍然使用动态数组(Vector)来演示:

template<class T, size_t N = 10>
class Array{
public:
	T& operator[](size_t index){
		return _array[index];
	}
	
	const T& operator[](size_t index) const{
		return _array[index];
	}
	
	size_t Size() const{
		return _size;
	}
	
	bool Empty() const{
		return _size == 0;
	}

private:
	T _array[N];		//非类型形参的使用
	size_t _size;
}

【注】:

  1. 浮点数类对象以及字符串是不允许作为非类型模板参数的。
  2. 非类型的模板参数必须在编译期就能确认结果。

类模板的特化

通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,比如:

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

这个函数模板比较的对象是整型家族元素,如果出现浮点型或者字符串类型就无能为力了,例如下面场景,函数模板的比较就是有问题的。

int main(){
	char p1[] = "giturtle";
	char p2[] = "giturtle";
	if(IsEqual(p1, p2))
		cout << "Yes" << endl;
	else
		cout << "No" << endl;

	return 0;
}

因为对于字符串,我们希望通过字典序进行对比。此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式

模板特化中分为:

  • 函数模板特化
  • 类模板特化

函数模板特化

函数模板的特化步骤:

  1. 必须要先有一个基础的函数模板
  2. 关键字template后面接一对空的尖括号<>
  3. 尖括号中指定需要特化的类型
  4. 函数形参表:必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
template<>
bool IsEqual<char*>(char*& left, char*& right){
	if(strcmp(left, right) == 0)		//字典序比较
		return true;
	return false;
}

【注】:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。

bool IsEqual(char* left, char* right){
	if(strcmp(left, right) == 0)
		return true;
	return false;
}

类模板特化

全特化

全特化:是将模板参数类表中所有的参数都确定化

template<class T1, class T2>
class Data{
public:
	Data() {cout<<"Data<T1, T2>" <<endl;}
private:
	T1 _d1;
	T2 _d2;
};

template<>
class Data<int, char>{
public:
	Data() {cout<<"Data<int, char>" <<endl;}
	private:
	int _d1;
	char _d2;
};

int main(){
	Data<int, int> d1;
	Data<int, char> d2;
	
	return 0;
} 

输出结果

Data<int, int> 
Data<int, char> 

可见全特化之后的版本成功匹配了某些场景,成为了特定情况下最适合的类模板的实例化版本。

偏特化

偏特化:针对模版参数进一步进行条件限制设计。

偏特化分为:

  • 部分特化
  • 参数更进一步的限制

对于类模板:

template<class T1, class T2>
class Data{
public:
	Data() {cout<<"Data<T1, T2>" <<endl;}
private:
	T1 _d1;
	T2 _d2;
};
  1. 部分特化版本:
//特化第二个参数为 int 类型
template <class T1>
class Data<T1, int>{
public:
	Data() {cout<<"Data<T1, int>" <<endl;}
private:
	T1 _d1;
	int _d2;		//偏特化
}; 
  1. 参数更进一步的限制版本:
//两个参数偏特化为指针类型
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;
};

void test2 (){
	Data<double , int> d1; 		//特化的 int 版本
	Data<int , double> d2;	    //基础 的模板
	Data<int *, int*> d3; 		//特化的 指针 版本
	Data<int&, int&> d4(1, 2);  //特化的 引用 版本
}

输出结果

Date(T1,int);		// 调用特化的 int 版本
Date(T1,T2);		// 调用 基础 的模板
Data(T1*,T2*);		// 调用特化的 指针 版本
Data(T1&,T2&);		// 调用特化的 引用 版本

类型萃取

类型萃取是类模板特化的应用。

例如:实现一个通用的拷贝函数

  1. memcpy
template<class T>
void Copy(T* dst, const T* src, size_t size){
	memcpy(dst, src, sizeof(T)*size);
}

上述代码虽然对于任意类型的空间都可以进行拷贝,但是如果拷贝自定义类型对象就可能会出错,因为自定义类型对象有可能会涉及到深拷贝(比如string),而memcpy属于浅拷贝。如果对象中涉及到资源管理,就只能用赋值。

  1. 赋值方式拷贝
template<class T>
void Copy(T* dst, const T* src, size_t size){
	for(size_t i = 0; i < size; ++i){
		dst[i] = src[i];
	}
}

用循环赋值的方式虽然可以,但是代码的效率比较低,而C/C++程序最大的优势就是效率高。那能否将另种方式的优势结合起来?遇到内置类型就用memcpy拷贝,遇到自定义类型就用循环赋值方式来做?可以,设定一个flag即可。

  1. 增加bool类型区分自定义与内置类型
template<class T>
void Copy(T* dst, const T* src, size_t size, bool flag){
	if(flag)
		memcpy(dst, src, sizeof(T)*size);
	else{
		for(size_t i = 0; i < size; ++i)
		dst[i] = src[i];
	}
}

通过多增加一个参数,就可将两种拷贝的优势体现结合起来。
但缺陷是:用户需要根据所拷贝元素的类型去传递第三个参数,那出错的可能性就增加。那能否让函数自动去识别所拷贝类型是内置类型或者自定义类型呢?

  1. 使用函数区分内置于自定义类型
    因为内置类型的个数是确定的,可以将所有内置类型集合在一起,如果能够将所拷贝对象的类型确定下来,在内置类型集合中查找其是否存在即可确定所拷贝类型是否为内置类型。
bool IsPODType(const char* strType){
	const char* arrType[] = {"char", "short", "int", "long", "long long", "float","double", "long double"};
	for(size_t i = 0; i < sizeof(array)/sizeof(array[0]); ++i){
		if(0 == strcmp(strType, arrType[i]))
			return true;
	}
	return false;
}

template<class T>
void Copy(T* dst, const T* src, size_t size){
	if(IsPODType(typeid(T).name()))
		memcpy(dst, src, sizeof(T)*size);
	else{
		for(size_t i = 0; i < size; ++i)
		dst[i] = src[i];
	}
}

通过typeid来确认所拷贝对象的实际类型,然后再在内置类型集合中枚举其是否出现过,既可确认所拷贝元素的类型为内置类型或者自定义类型。
但缺陷是:枚举需要将所有类型遍历一遍,每次比较都是字符串的比较,效率比较低。

  1. 类型萃取
    为了将内置类型与自定义类型区分开,给出以下2个类分别代表内置类型自定义类型
// 代表内置类型
struct TrueType{
	static bool Get(){
		return true ;
	}
};

// 代表自定义类型
struct FalseType{
	static bool Get(){
		return false ;
	}
};

给出以下类模板,将来用户可以按照任意类型实例化该类模板。

template<class T>
struct TypeTraits{
	typedef FalseType IsPODType;
};

对上述的类模板进行以下方式的实例化:

// 所有内置类型都需要特化一下:
template<>
struct TypeTraits<char>{
	typedef TrueType IsPODType;
};

template<>
struct TypeTraits<short>{
	typedef TrueType IsPODType;
};

template<>
struct TypeTraits<int>{
	typedef TrueType IsPODType;
};

template<>
struct TypeTraits<long>{
	typedef TrueType IsPODType;
};

通过对TypeTraits类模板重写4Copy函数模板,来确认所拷贝对象的实际类型。

/*
	T为int:TypeTraits<int>已经特化过,
		程序运行时就会使用已经特化过 TypeTraits<int>,
		该类中的 IsPODType 刚好为类 TrueType ,
		而 TrueType 中 Get 函数返回 true ,
		内置类型使用 memcpy 方式拷贝

	T为string:TypeTraits<string>没有特化过,
		程序运行时使用 TypeTraits 类模板, 
		该类模板中的 IsPODType 刚好为类 FalseType ,
		而 FalseType 中 Get 函数返回 false,
		自定义类型使用赋值方式拷贝
*/

template<class T>
void Copy(T* dst, const T* src, size_t size){
	if(TypeTraits<T>::IsPODType::Get())
		memcpy(dst, src, sizeof(T)*size);
	else{
		for(size_t i = 0; i < size; ++i)
			dst[i] = src[i];
	}
}

测试代码

int main() {
	int a1[] = { 1,2,3,4,5,6,7,8,9,0 };
	int a2[10];
	Copy(a2, a1, 10);
	for (const auto& e : a2) {
		cout << e << " ";
	}
	cout << endl;

	string s1[] = { "1111", "2222", "3333", "4444" };
	string s2[4];
	Copy(s2, s1, 4);
	for (const auto& e : s2) {
		cout << e << " ";
	}
	cout << endl;

	return 0;
}

输出结果

1 2 3 4 5 6 7 8 9 0
1111 2222 3333 4444

可见完成了我们的预期~

类型萃取有什么用呢?其实STL库中许多底层实现都是使用类型萃取的,有兴趣的看官可以去查看一下底层源码。


模板的分离编译

什么是分离编译?

一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式

那模板的分离编译有什么不同?

假如有以下场景,模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:

// a.h -> 头文件
template<class T>
T Add(const T& left, const T& right);		//声明
// a.cpp  -> cpp文件
template<class T>
T Add(const T& left, const T& right){		//实现
	return left + right;
}
// main.cpp	-> cpp文件
#include"a.h"
int main(){
	Add(1, 2);			//调用
	Add(1.0, 2.0);
	
	return 0;
}

因为在a.cpp中编译器没有找到对Add模板函数的的实例化,因此不会生成具体的加法函数。

main.obj中调用的Add<int>Add<double>,编译器在链接时才会寻址,单这两个函数没有实例化生成具体代码,因此链接时会报错。

解决方法

  1. 将声明和定义放到一个文件 “xxx.hpp” 里,或者xxx.h中也是可以的。
  2. 模板定义的位置显式实例化。但这种方法不实用,不推荐使用。

小结

【优点】:

  1. 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生。
  2. 增强了代码的灵活性

【缺点】:

  1. 模板会导致代码膨胀问题,也会导致编译时间变长
  2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误。

以上是C++模板相关知识的大概梳理,顺便贴上模板(上篇)的链接,有兴趣的博友可以传送过去看看,主要讲解:泛型编程函数模板类模板~

C++之 模板(上):【 https://blog.csdn.net/qq_42351880/article/details/100058341

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

giturtle

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值