当前位置: 首页 > article >正文

模版进阶 非类型模版参数

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

类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。

非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。

#include<iostream>
using namespace std;
#define N 100
template<class T>//静态的栈 也就是要控制大小固定
class stack
{
private:
	int _a[N];
	int _top;
};
int main()
{
	stack<int> s1;
	return 0;
}

这里我们是通过定义宏来固定栈的大小的 但是如果我们想要同时实现两个大小不同的栈要如何实现

#include<iostream>
using namespace std;
//#define N 100
template<class T,size_t N >//静态的栈 也就是要控制大小固定
class stack
{
private:
	int _a[N];
	int _top;
};
int main()
{
	stack<int,10> s1;//10
	stack<int,100>s2;//100
	return 0;
}

这里通过非类型的模版参数来实现同时定义不同大小的栈

这里的s1和s2两个栈 在本质上还是两个不同的类实现的  是两个不同的类型  只不过是将写不同类的工作交给了编译器去实现

这里需要注意的是 在c++20之前只允许整型作非类型模版的参数 

 c++20之后也支持double等内置类型 内置类型的指针也是可以的 

这里N已经是常量了 无法修改 但是这里没有报错是因为按需实例化

#include<iostream>
using namespace std;
//#define N 100
template<class T,size_t N >//静态的栈 也就是要控制大小固定
class stack//这里N已经是常量了 无法修改 但是这里没有报错是因为按需实例化
{
public:
	void func()
	{
		N++;
	}
private:
	int _a[N];
	int _top;
};
int main()
{
	stack<int,10> s1;//10
	stack<int,100>s2;//100
	//s1.func();
	return 0;
}

这里没有报错是因为按需实例化的原因 因为这里系统为了节约资源 不调用

是不会去检查很多的细节的  只有调用了 才会进行具体检查 

int main()
{
	stack<int,10> s1;//10
	stack<int,100>s2;//100
	s1.func();
	return 0;
}

类型模版可以给缺省值 非类型模版也可以给缺省值

template<class T,size_t N =1000 >//静态的栈 也就是要控制大小固定
class stack//这里N已经是常量了 无法修改 但是这里没有报错是因为按需实例化
{
public:
	void func()
	{
		N++;
	}
private:
	int _a[N];
	int _top;
};
int main()
{
	stack<int> s3;//这里使用了缺省值 1000大小
	//stack<int,10> s1;//10
	//stack<int,100>s2;//100
	//s1.func();
	return 0;
}

在我们的std库中也是有使用了非类型模版参数的结构的  可以看做用一个类去封装了静态数组

使用array去与普通的数组去做对比

int main()
{
	array<int,15> a1;
	int a2[15];
	return 0;
}

他们两者最大的区别是什么? 最大的区别是检查是否越界的能力 

对于普通的数组 他的越界检查能力是非常敷衍的 

int main()
{
	array<int,15> a1;
	int a2[15];

	a2[15];
	a2[16];
	a2[17];
	return 0;
}

这里对普通数组进行只读检查是无法检查出越界的 即便是第一个越界的位置15也无法检测

只有检查写时才能检查出来

并且在稍微远一点的越界位置无论读写都是无法检查出来的

而如果是array数组的话 则无论远近无论读写 都可以检查出来

这就是array与普通的数组最大的区别

 那么为什么array能够检查的这么仔细呢 因为array是自定义类型 在调用operator[]时 可以设立很多的检查

array的缺点 1.不负责自动初始化  2.他的开空间是不在堆上面 而是开在栈针上面    堆很大但是 栈针不大  所以array的设计也是非常鸡肋的 不如用vector

#include<iostream>
#include<array>
#include<vector>
using namespace std;
int main()
{
	vector<int> v = {1,2,3,4,5,6,7};
	vector<double> v1 = {1.1,2.2,3.3,4.4,5.5,6.6,7.7};
	int i = 1;
	int j = { 1 };
	int k  { 1};

	return 0;
}

这里的初始化方法是多样性的 这里有好有坏 是被使用者诟病的地方 这里会导致别人的误解

这里我们想要打印vector的内容 制作了一个遍历器函数 

void printvector(vector<int> v)
{
	std::vector<int>::iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		it++  ;
	}
	cout << endl;
}
int main()
{
	vector<int> v = {1,2,3,4,5,6,7};
	vector<double> v1 = {1.1,2.2,3.3,4.4,5.5,6.6,7.7};
	int i = 1;
	int j = { 1 };
	int k  { 1};
	printvector(v);
	//printvector(v1);
	return 0;
}

这里单个的类型的遍历是没有问题的 如果想要进行多个类型的遍历 就需要用到模版

using namespace std;
template <class T>
void printvector(vector<T> v)
{
	std::vector<T>::iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		it++  ;
	}
	cout << endl;
}
int main()
{
	vector<int> v = {1,2,3,4,5,6,7};
	vector<double> v1 = {1.1,2.2,3.3,4.4,5.5,6.6,7.7};
	int i = 1;
	int j = { 1 };
	int k  { 1};
	printvector(v);
	printvector(v1);
	return 0;
}

这里却会发生错误  这里正确写法需要再迭代器之前写一个typename

oid printvector(vector<T> v)
{
	typename std::vector<T>::iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		it++  ;
	}
	cout << endl;
}
int main()
{
	vector<int> v = {1,2,3,4,5,6,7};
	vector<double> v1 = {1.1,2.2,3.3,4.4,5.5,6.6,7.7};
	int i = 1;
	int j = { 1 };
	int k  { 1};
	printvector(v);
	printvector(v1);
	return 0;
}

那么这里的报错的原因是什么呢 而typename的作用又是什么呢?

这里的报错是因为编译器从上向下检查 检查到这个函数内部时 由于vector::iterator 这个还没有实例化 所以系统不会进行 仔细检查 所以iterator对于编译器来说现在是未知的 而vector类域指定下 可能会取到静态变量 类型  无法确定 所以报错 

而typename的作用就是给编译器声明一下 这里是一个类型 先跳过这里等到实例化之后再次进行检查编译  

还有另一种不加typename的方式 那就是直接使用auto

void printvector(vector<T> v)
{
	//typename std::vector<T>::iterator it = v.begin();
	auto it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		it++  ;
	}
	cout << endl;
}
int main()
{
	vector<int> v = {1,2,3,4,5,6,7};
	vector<double> v1 = {1.1,2.2,3.3,4.4,5.5,6.6,7.7};
	int i = 1;
	int j = { 1 };
	int k  { 1};
	printvector(v);
	printvector(v1);
	return 0;
}

这时我们的代码也是可以正常运行的

二.模版的特化

函数模版的特化 特化就是进行特殊化处理

template<class T>
bool Less(T left, T right)
{
	return left < right;
}
 int main()
 {
	 cout << Less(1, 2) << endl;

	 Date d1(2022, 7, 7);
	 Date d2(2022, 7, 8);
	 cout << Less(d1, d2) << endl;

	 Date* p1 = &d1;
	 Date* p2 = &d2;
	 cout << Less(p1, p2) << endl;  // 调用特化之后的版本,而不走模板生成了
	 return 0;
 }

这里是对日期类的比较大小

这里的第三个比较使用的是指针  那么日期类的指针比较的大小不在是日期的的大小比较 而是变成了比较指针地址的大小 这是我们不想要的结果 这时就需要用到函数模版特化去解决

template<class T>
bool Less(T left, T right)
{
	return left < right;
}

 //对Less函数模板进行特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
return *left < *right;
 }

 int main()
 {
	 cout << Less(1, 2) << endl;

	 Date d1(2022, 7, 7);
	 Date d2(2022, 7, 8);
	 cout << Less(d1, d2) << endl;

	 Date* p1 = &d1;
	 Date* p2 = &d2;
	 cout << Less(p1, p2) << endl;  // 调用特化之后的版本,而不走模板生成了
	 return 0;
 }

这时通过特化版本专门为指针设计了一个版本来比较大小实现

函数模版特化也是有缺点的

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

 //对Less函数模板进行特化
template<>
bool Less<Date*>(const Date* & left,const Date* &right)
{
return *left < *right;
 }

这时const修饰下 对于特化模版的处理是要不同的 

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

 //对Less函数模板进行特化
template<>
bool Less<Date*>(Date* const  & left, Date* const  &right)
{
return *left < *right;
 }

这才是正确的写法 

原因是原模版中const修饰的T类型的内容 也就是指针指向的内容    而将const放在data*前面 则会修饰到指针 并没有修饰到指针指向的内容 所以要将data*放在const之前 让const修饰到指针指向的内容

最好是不使用特化模版 而是直接去创建一个解决日期类指针比较大小的函数

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

 //对Less函数模板进行特化
//template<>
//bool Less<Date*>(Date* const  & left, Date* const  &right)
//{
//return *left < *right;
// }
bool Less(Date* left, Date* right)
{
	return *left < *right;
}
 int main()
 {
	 cout << Less(1, 2) << endl;

	 Date d1(2022, 7, 7);
	 Date d2(2022, 7, 8);
	 cout << Less(d1, d2) << endl;

	 Date* p1 = &d1;
	 Date* p2 = &d2;
	 cout << Less(p1, p2) << endl;  // 调用特化之后的版本,而不走模板生成了
	 return 0;
 }

这样也可以正常使用

 编译器的调用规则时有现成就用现成的之后在使用模版

所以一个模版一个特化模版 一个函数的情况下会优先使用 函数

接下来介绍类的特化模版

// 类模板
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; }
};

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

	 return 0;
 }

在原模版中写过私有成员之后 在特化模版的类中就不需要在写一次了

这里的特化模版是通过不同的参数类型去调用的 

还有偏特化 或者称为半特化

template<class T1>
class Data<T1, char>
{
public:
	Data() { cout << "Data<T1, char>- 半特化" << endl; }
};

二级指针也算是指针

template<>
class Data<int, char>
{
public:
	Data() { cout << "Data<int, char>- 全特化" << endl; }
};
//偏特化 /半特化
template<class T1>
class Data<T1, char>
{
public:
	Data() { cout << "Data<T1, char>- 半特化" << endl; }
};
//两个参数偏特化为指针类型 
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;
};
 int main()
 {
	 Data<int, int>d1;
	 Data<int, char>d2;
	 Data<double, char>d3;
	 Data<int*, int*>d4;
	 Data<int&, int&> d5(1,2);
	 return 0;
 }

这里不仅可以特化普通的类型还可以特化指针和引用 指针中的特化是一个大类 其中的二级指针三级指针都可以使用这个大的特化类

3.模版分离编译 

//func.h
template<class T>
T Add(const T& left, const T& right);
//func.cpp
template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}
// test.cpp
#include"func.h"
int main()
{
	Add(1, 2);
	Add(1.0, 2.0);

	return 0;
}


这里我们采用类比法来进行比对

//func.h
void func();
//func.cpp
void func()
{
	cout << "func" << endl;
}
// test.cpp
#include"func.h"
int main()
{
	//Add(1, 2);
	//Add(1.0, 2.0);
	func();
	return 0;
}

这时我们可以发现普通的函数声明和定义分离是可以正常使用的

但是对于函数模版来说声明和定义分离却无法正常使用 

在test.cpp中调用add函数 这里有声明知道要将参数定义为什么样的类型 但是对于func.cpp中只有定义却没有声明不知道要将其参数设置为什么类型 因此add不会被编译 生成指令也就没有将add的地址放入到符号表当中去 所以链接也就找不到

解决方法 显示实例化

//func.cpp
#include<iostream>
using namespace std;
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);

这时候代码就可以正常使用了

但是函数模版声明和定义分离也是有弊端的 需要不停的去加入显示实例化 所以最好的解决方法就是不要去进行声明和定义分离

对于类模版来说 长点的成员函数 声明和定义分离 写到当前文件类外面 短的可以直接定义在类里面 默认就是inline

   模板总结 【优点】

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

2. 增强了代码的灵活性

【缺陷】

1. 模板会导致代码膨胀问题,也会导致编译时间变长

2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误 


http://www.kler.cn/news/341953.html

相关文章:

  • 嵌入式开发:STM32 硬件 CRC 使用
  • yolov5-7.0模型DNN加载函数及参数详解(重要)
  • SQL自学:什么是SQL的聚集函数,如何利用它们汇总表的数据
  • 【电路笔记】-求和运算放大器
  • JS | 如何解决ajax无法后退的问题?
  • 使用纯CSS和JavaScript来实现一个转盘抽奖效果
  • 微信小程序15天
  • Linux中定时删除10天前的日志文件
  • HTTPS协议和密码套件在AWS中的应用
  • vue2项目的路由使用history模式,刷新会导致页面404的问题
  • [SAP ABAP] LIKE TABLE OF
  • 按分类调用标签 调用指定分类下的TAG
  • 等保测评1.0到2.0的演变发展
  • 保护企业知识产权!推荐十款源代码加密软件
  • UM-Net: 重新思考用于息肉分割的ICGNet,结合不确定性建模|文献速递-基于多模态-半监督深度学习的病理学诊断与病灶分割
  • 压缩包格式详解:RAR、ZIP、7z等格式的优劣与使用场景
  • Linux进程间通信(个人笔记)
  • JavaScript编程语言
  • 如何进行数据中心负载测试的自动化?
  • Java之方法