C++模板进阶


在这里插入图片描述

系列文章目录

C++模板的使用

一、前言

模板初阶我们了解了什么是模板,如何使用模板。函数模板和类模板都是模板。


二、非类型模板参数

1.非类型模板概念

模板参数分类类型形参与非类型形参。

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

观察下面代码:

#include <iostream>
using namespace std;

#define N 10

template<class T>
class Stack
{
private:
	int _a[N];
	int _top;
};

如代码中我们使用模板定义的一个栈类。需要修改成员变量数组_a[N]只需要修改宏定义就好了。
这样定义有一个缺陷,如果想要一个栈存100的数据一个栈存10的数据,这样>使用宏定义是无法完成的
所以为了更加方便,非类型模板参数规定可以使用常量参数。

Stack<int,100> st1; //100
Stack<int,10> st2; //10

思考一个问题,这俩个栈是一个类出来的吗

很明显不是一个类出来的。是俩个不同的类。
模板本质是让编译器去帮我们写代码。实际上和手写没有什么区别。

2.参数注意事项

  • 浮点数、类对象以及字符串不允许作为非类型模板参数的。
  • 非类型的模板参数必须在编译期就能确认结果
//c++20之前,只能允许整形做非模板参数
//c++20之后,可以支持double等其他内置类型
template<double x,int* ptr>
class A
{};

c++20之后就可以使用double。

观察下面代码:

#include <iostream>
using namespace std;
template<class T,size_t N>
class Stack
{
public:
	void func()
	{
		N++;
	}
private:
	int _a[N];
	int _top;
};
int main()
{
	Stack<int,100> st1; 
	//st1.func();//按需实例化
	return 0;
}

上面代码中有俩个问题。

  • 未调用对象st1的函数,不会报错,这是由于模板的按需实例化,是指模板在未实例化之前,只会对外壳进行检查,内部的细节问题不会进行检查。由于函数不在类中,只有当对象调用函数时,函数才会被实例化出来。
    编译器按需实例化
  • 由于非类型模板参数是常量,所有不能对常量N进行++操作等。
    非类型模板参数是常量
    按需实例化是指:将类成员变量实例化,如果后面未使用到成员函数,则不会将函数进行实例化
  • 非类型模板参数和类型模板参数一样是可以给缺省值的
    在这里插入图片描述

3.array类

C++11库中有一个使用了非类型模板参数的类。是一个固定大小的数组。
在这里插入图片描述
可以将其理解为用一个类封装了静态数组。它支持迭代器操作。其迭代器就是俩个原生指针
与普通的数组如:int arr[]
如果越界访问,会进严格的检测(assert断言).
array开辟的空间在栈上,所以不能太大。
array不会进行初始化,只是把空间开辟出来。
所以我们一般使用vector<>。vector的功能已经很完善了。

三、模板typename

1.使用函数模板遍历容器

观察下面代码:

#include <iostream>
#include <vector>
using namespace std;
template<class T>
void PrintVector(const vector<T>& v)
{
	vector<T>::const_iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << ' ';
		++it;
	}
	cout << endl;
}
int main()
{
	vector<int> v1 = { 1, 2,3,4,5,6 };
	vector<double> v2 = { 1.1,2.2,3.3,4.4,5.5,6.6 };
	PrintVector(v1);
	PrintVector(v2);
	return 0;
}

代码逻辑没有问题,但是执行代码时:
在这里插入图片描述
编译报错。由于模板特性,模板不会对代码进行详细的检查,只会将模板的外壳进行检查
在这里插入图片描述
这时就要对该行进typename明确告诉编译器这个是一个类型.
在这里插入图片描述
上图这样实例化的类就是正确的,编译器知道这一个int类型的类。

	typename vector<T>::const_iterator it = v.begin();

另一种方法可以避免这一问题:

	auto it = v.begin();

auto是类型,v.begin()返回什么auto就是什么。
修改之后的结果:
在这里插入图片描述

四、模板的特化

1.函数模板的特化

使用日期类Date进行举例:

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
	bool operator<(const Date& d)const
	{
		return (_year < d._year) ||
			(_year == d._year && _month < d._month) ||
			(_year == d._year && _month == d._month && _day < d._day);
	}
	bool operator>(const Date& d)const
	{
		return (_year > d._year) ||
			(_year == d._year && _month > d._month) ||
			(_year == d._year && _month == d._month && _day > d._day);
	}
	friend ostream& operator<<(ostream& _cout, const Date& d);
private:
	int _year;
	int _month;
	int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
	_cout << d._year << "-" << d._month << "-" << d._day;
	return _cout;
}

使用模板进行不同类型的比较:

template<class T>
bool Less(T left, T right)
{
	return left < right;
}
int main()
{
	Date d1(2022, 10, 1);
	Date d2(2022, 10, 2);
	cout << Less(d1, d2) << endl;
	return 0;
}

这样的代码只支持非指针和引用的比较。所以我们还需要写一个支持指针类型的模板。c++语法中我们可以对原有的模板进行特化

template<class T>
bool Less(T left, T right)
{
	return left < right;
}
// 特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
	return *left < *right;
}

该特化是指,其他类型模板实例化运行第一个模板Date*类型运行第二个模板。特化就是对某个类型进行特殊处理,如果匹配到该特化走特化版本的模板
函数模板很容易出错:

template<class T>
bool Less(const T& left,const T& right)
{
	return left < right;
}
// 特化
template<>
bool Less<Date*>(const Date*& left, const Date*& right)
{
	return *left < *right;
}

在我们的理解中,将T替换成我们需要特化类型就可以进行特殊处理,但是类似于上面的代码,稍微不细心就会出错
在这里插入图片描述
正确的特化方式:

// 特化
template<>
bool Less<Date*>(Date* const & left, Date*const & right)
{
	return *left < *right;
}

由于函数是可以重载的,所以函数模板我们尽量不使用模板特化:

bool Less(Date* const & left, Date*const & right)
{
	return *left < *right;
}

2、类模板的特化

全特化

特化形式:

//类模板
template<class T1,class T2>
class Date
{
public:
	Date() { cout << "Date<T1,T2>原模版" << endl; }
private:
	T1 _d1;
	T2 _d2;
};
//类模板特化
template<>
class Date<int, char>
{
public:
	Date() { cout << "Date<int,char>全特化" << endl; }
};

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

d2的类型为Date<int,char>符合特化的形式,实例化时执行特化版本
代码执行结果:
在这里插入图片描述
将俩个模板参数都明确规定的特化称为全特化

偏特化非指针类型

类似的,还有偏特化:

//类模板的偏特化
template<class T1>
class Date<T1, int>
{
public:
   Date() { cout << "Date<T1,int>偏特化" << endl; }
};

该特化的类型为:只要第二个int的类型就执行该特化。

int main()
{
	Date<int, int> d1;
	Date<int, char> d2;
	Date<double, int> d3;
	return 0;
}

结果:
在这里插入图片描述

偏特化指针类型

如果T1,T2是指针就会施行该特化

template<class T1, class T2>
class Date<T1*, T2*>
{
public:
	Date() {
		cout << typeid(T1).name() << endl;
		cout << typeid(T2).name() << endl;
		cout << "Date<T1*,T2*>偏特化" << endl; 
	}
};

注意
class Date<T1*, T2*>T1*T2*是其表示作用,是用于匹配,参数是否为指针,一级指针、二级指针、函数指针都是指针
所以该特化表明如果是指针就执行该特化
通过结果来进行观察:

int main()
{
	Date<int*, int*> d1;
	Date<int**, char**> d2;
	Date<int**, double*> d3;
	return 0;
}

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

模板特化引用

和特化指针一样:

//偏特化特化引用类型
template<class T1, class T2>
class Date<T1&, T2&>
{
public:
	Date() {
		cout << typeid(T1).name() << endl;
		cout << typeid(T2).name() << endl;
		cout << "Date<T1&,T2&>偏特化" << endl; 
	}
};

测试:

int main()
{
	Date<int&, double&> d4;
	return 0;
}

结果:
在这里插入图片描述
也可以一个引用一个指针

五、模板的分离编译

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

模板的分离编译不能分文件

//a.h中
template<class T>
T Add(const T& left, const T& right);

//a.cpp中
template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}

//主函数中
// main.cpp
#include"Template.h"
int main()
{
	Add(1, 2);
	Add(1.0, 2.0);
	return 0;
}

结果:
在这里插入图片描述
发生了链接错误。
在这里插入图片描述
编译器对模板进行处理时,在a.o中对模板只是进行外壳的维护实例化的阶段是在编译阶段运行的
调用的地方只有声明,没有定义
定义的地方,不知道实例化成什么
解决方法是在定义的地方进行显示实例化

//a.cpp中
template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}
template
int Add(const int& left, const int& right);
template
double Add(const double& left, const double& right);

但是显示实例化具有局限性,原本就是需要靠模板来对各个类型进行处理,我们再去显式实例化模板就失去了它的意义

如果编译阶段,查找其他cpp文件实例化成了什么,编译速度会大大降低。所以在模板的分离编译时,我们需要在同一个文件下进行分离
同一个文件下,就有声明和定义在一起。就不需要进行链接,当前文件就有函数的地址。
在实际开发中,同一个文件下的声明和定义分离,在类模板中很常见。

六、模板总结

[优点]

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

[缺陷]

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

如果你有所收获,可以留下的你点赞和关注,谢谢你的观看!!!

  • 17
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值