C++之模板进阶

发布于:2022-11-09 ⋅ 阅读:(1027) ⋅ 点赞:(0)

前言

在前面我们讲了模板的初阶的内容,其内容还是比较简单的,但是关于模板的内容并非只有那些,下面我们就来看一看模板进阶的内容。

一、非类型模板参数

在讲非类型模板参数之前,我们先讲一个在C语言中经常使用的语法,#define.

#define N 100
template<class T>
class array
{
private:
	T _a[N];
};

上面的这一种方式使我们在定义常量或者定义一个数组的时候经常使用的方式,但是这种方式也存在弊端,就比如上面我们定义了一个模板类型的静态数组,让其长度为100。
这时就有一个问题: 如果要开辟N为100的空间,那么以这种方式要define为100.
但是如果要开辟N为1000的空间,那么只能修改N为1000,那么如果你还要开辟N为100的空间,那么就只能进行取舍,让N为1000,这样就会很浪费空间,所以出现了非类型模板参数,意思就是已经指定好类型了,如下

namespace ljx
{
	template<class T, size_t N = 10>
	class array
	{
	private:
		T _a[N];
	};
}

大家可以看到这里template<>中的第二个就是就是非模板参数,我们将它定义为了 size_t类型,这里默认N为10,当我们传入其他的数据的时候,N就是我们传入的数值。看下面的例子:

void test()
{
	ljx::array<int> a1;//默认为10
	ljx::array<int, 100> a2;//100
	ljx::array<int, 1000> a3;//1000
}

模板参数分为类型形参与非类型形参
类型形参: 出现在末班参数列表中,跟在class或者typename之后的参数类型名称。
非类型形参: 就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
注意:
1.浮点数、类对象以及字符串是不允许最为非类型模板参数的。
2.非类型的模板参数必须在编译期就确认结果。

二、模板的特化

1.概念

通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型可能会得到一些错误的结果,需要特殊处理,比如下面我们实现了一个专门用来进行小于比较的模板参数。

	class Date
{
public:
	Date(int year = 0,int month = 0,int day = 0)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
	
	bool operator<(const Date& d) const
	{
		if ((_year < d._year)
			|| (_year == d._year && _month < d._month)
			|| (_year == d._year && _month == d._month && _day < d._day))
		{
			return true;
		}
		else
			return false;
	}
	
	bool operator>(const Date& d) const
	{
		if ((_year > d._year)
			|| (_year == d._year && _month > d._month)
			|| (_year == d._year && _month == d._month && _day > d._day))
		{
			return true;
		}
		else
			return false;
	}
	
private:
	int _year;
	int _month;
	int _day;
	
};

//函数模板
template<class T>
bool less(T left, T right)
{
	return left < right;
}

int main
{
	cout << ljx::less(1, 2) << endl;  //结果正确
	Date d1(2022, 11, 7);
	Date d2(2022, 11, 8);
	cout << ljx::less(d1, d2) << endl;//结果正确
	Date* p1 = &d1;
	Date* p2 = &d2;
	cout << ljx::less(p1, p2) << endl;//结果错误
	return 0;
}

大家看上面的这个例子,我们自己写了一个函数用来比较传入数据的大小,前面我们已经写了关于Date类的大于和小于的比较,所以这里可以直接比较Date类型数据的大小,对于前面两种调用来说,我们的得到的结果和预期的一样,但是后面的比较结果就不对了,我们这里传入的虽然是地址,但是我们的本意是让d1与d2进行比较,但是这里比较的是两个指针的大小,所以之前的函数模板就不能应用了,我们就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。 模板特化中分为函数模板特化类模板特化

2.函数模板特化

函数模板的特化步骤:

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

int main
{
	cout << ljx::less(1, 2) << endl; 
	Date d1(2022, 11, 7);
	Date d2(2022, 11, 8);
	cout << ljx::less(d1, d2) << endl
	Date* p1 = &d1;
	Date* p2 = &d2;
	cout << ljx::less(p1, p2) << endl;
	//调用特化之后的版本,就不走模板生成了
	return 0;
}

有了这个特化以后传入Date*的变量就会先将其解引用然后在进行比较,这样就符合我们的意愿了,而且最后输出的结果也和我们所想的一样。
注意: 一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。

bool less(Date* left, Date* right)
{
	return *left < *right;
}

这种实现简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化时特别给出,因此函数模板不建议特化。

3.类模板特化

(1)全特化

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

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<T1,T2>
	Data<int, char> d2;        //Data<int,char>
	Data<int, double> d3;      //Data<T1,T2>
	return 0;
}

(2)偏特化

偏特化:任何针对模板参数进一步进行条件限制设计的特化版本。比如对我们上面所说的模板进行偏特化
后面为了代码的简介,我们就直接把定义变量的部分去掉了。

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

特化的两种表现方式:
1.部分特化
将模板参数列表中的一部分参数特化

template<class T1>
//把第二个参数特化为char
class Data<T1, char>
{
public:
	Data()
	{
		cout << "Data<T1,char>" << endl;
	}
};

2.参数更进一步的限制
偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。

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

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

结合我们前面所写的全部的模板,实验结果如下

int main()
{
	Data<int, int> d1;         //Data<T1,T2>
	Data<int, char> d2;        //Data<int,char>
	Data<int, double> d3;      //Data<T1,T2>
	Data<double, char> d4;     //Data<T1,char>
	Data<int*, double*> d5;    //Data<T1*,T2*>
	Data<int&, double&> d6;    //Data<T1&,T2&>
	return 0;
}

4.类模板特化应用实例

假设有如下面的我们自己实现的专门用来按照小于比较的类模板less

struct Date
{
	Date(int year = 0, int month = 0, int day = 0)
		:_year(year)
		, _month(month)
		, _day(day)
	{}
	bool operator<(const Date& d) const
	{
		if ((_year < d._year)
			|| (_year == d._year && _month < d._month)
			|| (_year == d._year && _month == d._month && _day < d._day))
		{
			return true;
		}
		else
			return false;
	}
	bool operator>(const Date& d) const
	{
		if ((_year > d._year)
			|| (_year == d._year && _month > d._month)
			|| (_year == d._year && _month == d._month && _day > d._day))
		{
			return true;
		}
		else
			return false;
	}
	
	int _year;
	int _month;
	int _day;
};

namespace ljx
{
	template<class T>
	struct less
	{
		bool operator()(const T& x,const T& y)
		{
			return x < y;
		}
	};
}

这里我们还是以自己定义的Date类来举例子,大家可以看到,在Date类中我们已经进行了运算符重载,可以直接使用。

int main()
{
	Date d1(2022, 11, 8);
	Date d2(2022, 11, 9);
	Date d3(2022, 11, 10);
	Date d4(2022, 11, 1);
	priority_queue<Date, vector<Date>, ljx::less<Date>> pq1;
	pq1.push(d1);
	pq1.push(d2);
	pq1.push(d3);
	pq1.push(d4);
	while (!pq1.empty())
	{
		const Date& top = pq1.top();
		cout << top._year << "/" << top._month << "/" << top._day << endl;
		pq1.pop();
	}
	cout << endl;

	priority_queue<Date*, vector<Date*>, ljx::less<Date*>> pq2;
	pq2.push(new Date(2022, 11, 8));
	pq2.push(new Date(2022, 11, 9));
	pq2.push(new Date(2022, 11, 10));
	pq2.push(new Date(2022, 11, 1));
	while (!pq2.empty())
	{
		Date* top = pq2.top();
		cout << top->_year << "/" << top->_month << "/" << top->_day << endl;
		pq2.pop();
	}
	return 0;
}

在上面我们以优先级队列作为容器,进行举例。我们知道优先级队列的底层实现其实是堆算法,我们再传入less仿函数,这样就可以形成一个大堆,依次取堆顶的数据的话就可以以日期从大到小的顺序进行排列。
为了证明特化的作用,我们还构建了一个存储Date数据的优先级队列,下面我们先看看结果。
在这里插入图片描述
第一个存储Date数据的优先级队列进行一系列取堆顶的操作以后就可以得到一个按照降序排列的数据,而存储Date
数据的队列则是杂乱无章的(这里其实是根据地址排序的,所以每次运行的结果都可能是不一样的),这时候类模板的特化就发挥作用了,我们看看修改以后的数据是否和我们想的一样。

namespace ljx
{
	template<class T>
	struct less
	{
		bool operator()(const T& x,const T& y)
		{
			return x < y;
		}
	};
	//类模板特化
	template<>
	struct less<Date*>
	{
		bool operator()(Date* x, Date* y)
		{
			return *x < *y;
		}
	};
}

在这里插入图片描述
再次运行两个队列操作后的结果就相同了,这也就是吗,类模板特化的作用。

三、模板的分离编译

1.什么是分离编译

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


在讲述模板的分离编译之前,我们先给大家举一个没有进行分离编译的场景。
在这里插入图片描述
通过图中我们可以看到这里是可以正常运行的。

2.模板的分离编译

然后我们再来看看模板分离编译以后的结果
在这里插入图片描述
大家可以看出这里报出了链接错误,为什么出现链接错误呢???
我们看下面的分析
在这里插入图片描述
可能有的小伙伴看了上面的过程分析以后还是不明白,简单的来说:
就是这里template.h文件和template.cpp文件是分离的,当你进行实例化的时候是.h中进行了实例化,但是.cpp文件名中的函数并没有进行实例化,所以在编译的时候他的地址也是找不到的,所以我们后面才会出现链接错误。

3.解决方法

  1. 将声明和定义放到一个文件 “xxx.hpp” 里面或者xxx.h其实也是可以的。推荐使用这种。
  2. 模板定义的位置显式实例化。这种方法不实用,不推荐使用。
    大家只需要知道第一种都定义在同一个文件中就可以了,第二种不需要掌握

四、模板总结

优点:
1.模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
2.增强了代码的灵活性
缺陷:
1.模板会导致模板膨胀问题,也会导致编译时间变长
2.出现模板编译错误时,错误信息非常凌乱,不易定位错误

总结

本次关于模板进阶的内容到这里就结束了,希望大家能够有所收获,正式因为有了模板,C++使用起来才会十分的便捷,这也是他和C语言最大的差别之一,大家下来可以自己尝试一下上面所列举的一些场景,相信会加深大家对模板的理解

本文含有隐藏内容,请 开通VIP 后查看