C++模版进阶

目录

前言:

1.非类型模版参数

2.函数模版的(全)特化

3.类模版的特化 

全特化:

偏特化:

4.模版的分离和编译

模版声明定义分离的问题:

解决方法:

a.显示实例化

b.声明定义放到一个文件,前声明,后定义

前言:

本篇来介绍模版的更复杂更多样化的一些用法,方便后面的学习与使用,模版的初阶见:模版初阶

1.非类型模版参数

//非类型模版参数
//template<class T,size_t N>
//class Array
//{
//public:
//	Array()
//	{}
//private:
//	T _a[N];
//};
//
//template<class T,bool flag=false>
//void func(const T a)
//{
//	int b = 0;
//	if (flag)
//		b = a;
//	cout << b << endl;
//}

 非类型模版参数就是常量。

如代码也可以是bool值等等,好处就是相比于#define定义的宏,之后就不能修改了,而非类型模版参数相比灵活一些:

//int main()
//{
//	Array<int, 10> a1;
//	Array<double, 20> a2;
//
//	func<int,true>(2);
//
//	return 0;
//}

Array类传的第二个模版参数是多少,类中的N就是多少。

func函数第二个模版参数可以传布尔值(当然可以是0和1),传的是多少,func函数中flag就是多少 ,如果不穿,就用默认的false。

那非类型模版参数有什么用呢?在数据结构 ​​​​​​位图的实现中会使用。

2.函数模版的(全)特化

首先,需要我们注意,不论是函数模版的特化还是下面的类模版的特化,都必须基于一个父模版。

例如:

我们要来比较下面的类型,就可提供一个函数,并借助函数模版:

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

int main(){
	cout << Less(1, 2) << endl;//结果1
	
	Date d1(2024,5,13);
	Date d2(2023,5,13);

	cout << Less(d1, d2) << endl;//结果0
    return 0;
}

注意d1与d2比较时,会调用Date类中的operator<()进行比较。

 但是下面的情况该怎么解决呢?能直接比较吗?

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

	Date d1(2024,5,13);
	Date d2(2023,5,13);

    Date* p1 = &d1;
	Date* p2 = &d2;
	cout << Less(p1, p2) << endl;//不特化,结果比较的就是指针指向空间地址的大小,结果一直为真

 如果直接调用普通的函数模版,T被实例化为指针,那比较的就是指针了,指针比较比较的是什么?是指针指向空间的地址的大小,如果p2的地址大小大于p1的,那比较的结果会永远为真。

所以我们使用函数模版的方式怎么解决?使用函数模版的特化:

//函数模版的特化---针对Date*类型的全特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
	return *left < *right;//指针比较大小比较的是指向空间地址的大小,但是解引用就是比较的Date类型,就会调用Date的operator<
}

当T被实例化为Date*时,就会去调用模版的特化,所以比较p1p2就会先解引用,找到指针指向的内容也就是d1与d2,然后再比较就会调用Date的operator<()了。

其实我们一般使用函数的重载来解决这样的问题,可读性更高,并且代码简洁明了:

//但是函数模版的特化没有直接函数重载可读性高,简单明了
//bool less(Date* left, Date* right)
//{
//	return *left < *right;
//}

如果函数重载与函数模版特化都存在,那会调用函数重载,因为函数在编译阶段就call到地址了,而模版还需要实例化。 

上面的特化称为全特化,模版还有偏特化,偏特化在类模版中说明,而函数模版是不支持偏特化的:

//函数模版不允许使用偏特化
//template<class T>
//bool Less<T*>(T* left, T* right)
//{
//	return *left < *right;
//}

在数据结构篇哈希表的实现中会使用。

3.类模版的特化 

正常我们来进行上面的比较都会提供一个仿函数,而仿函数是封装在一个类里面的:

template<class T>
struct Greater
{
	bool operator()(const T x, const T y)
	{
		return x > y;
	}
};

int main(){
	cout<<Greater<int>()(1, 2)<<endl;//结果0

	Date d3(2025, 5, 13);
	Date d4(2026, 5, 13);

	cout << Greater<Date>()(d3, d4) << endl;//结果0
    return 0;
}

但是也是会碰到上面对于指针的比较,如果直接使用普通类模版,比较的也是指针指向空间地址的大小。所以提供一个类模版的全特化。

全特化:
//类模版的全特化---针对Date*的全特化
template<>
struct Greater<Date*>
{
	bool operator()(const Date* x, const Date* y)
	{
		return *x > *y;//解引用后调用Date的operator>;如果不特化,指针的比较就按指向空间的地址比了
	}
};

这样先解引用,找到指针指向的内容,也就是d3d4对象,再比较会调用Date的operator<()。 

偏特化:

那如果传的类型是int*等等呢?需要我们一个一个写针对某个指针类型的全特化,这时,就可以使用偏特化来解决问题:

//针对指针的偏特化
template<class T>
struct Greater<T*>
{
	bool operator()(const T* x, const T* y)
	{
		return *x > *y;
	}
};

int main(){
	int* p5 = new int(1);
	int* p6 = new int(2);

	cout << Greater<int*>()(p5, p6) << endl;
    return 0;
} 

这样只要模版实例化为指针类型,就会调用指针的偏特化,引用也可以这样玩。

注意,如果指针的偏特化与Date*这样的全特化一起存在时,传模版参数是Date*会调用哪一个?

编译器当然是调用最匹配的那一个,也就是针对Date*的全特化,因为全特化的限制更多。

 偏特化还可以用来进行部分特化:
 

template<class T1,class T2>
class Test
{
public:
	Test()
	{
		cout << "Test<T1,T2>" << endl;
	}
private:
	T1 _t1;
	T2 _t2;
};

//部分特化
template<class T1>
class Test<T1,int>
{
public:
	Test()
	{
		cout << "Test<T1,int>" << endl;
	}
private:
	T1 _t1;
	int _t2;
};

int main()
{
	//调用部分特化
	Test<int, int> t1;
	
	//调用部分特化
	Test<double, int> t2;

	//调用普通的类模版(父模版)
	Test<int, double> t3;
	return 0;
}

4.模版的分离和编译

首先,我们先复习一下:

如果函数的声明与定义分离,那么程序运行,函数就会在链接的时候确定地址。

如果声明定义放到一起,那么在编译阶段就可以call到这个函数的地址了。 

 

如上图也可以知道如果声明定义分离,在链接的时候才会合并文件,这样在链接时就能找到函数的实现,才能确定函数的地址。

模版声明定义分离的问题:

 

那模版的声明与定义分离也是这个问题,声明与定义分离会导致在声明文件中(也就是头文件中),生成了目标文件时模版还没有实例化,那就确定不了函数的地址,主文件生成的目标文件时模版也没有实例化,也确定不了函数的地址,链接时就会发生错误。也就是在生成了目标文件时还都没有实例化,这是编译器做出的处理,那能不能让编译器做到在编译阶段(也就是生成.s文件的时候)就进行实例化呢?不能,因为对于大型工程来说,会导致编译的效率变慢很多。

解决方法:
a.显示实例化

 

坏处就是需要什么类型就要实例化一份出来。 

b.声明定义放到一个文件,前声明,后定义

 

声明与定义放到一起,这样模版直接就能实例化了(在什么时候实例化呢?在编译的时候实例化)编译的时候就能call到这个函数的地址了,不用再进行链接了。

那类模版声明定义分离有意义吗?

如果成员函数在类中定义,编译器可能会将它当做内联函数处理,所以在stl源码里面有些短小的函数直接就放在类里面定义了,就是当做了内联函数,有些大一点的函数就类中声明,类外定义,源码就这样搞的,所以声明定义分离还是有一定的道理的(什么道理?)。

另外,有些地方可能会定义头文件为.hpp,但实际也是头文件,只是暗示这是模版,声明定义写到一起了,下图为boost库:

总结:

膨胀就是指实例化出各种各样的模版参数,文件会变大,因为只要一实例化,就是一份新的代码,但是不可避免。

编译时间变长是因为模版实例化的时候是在编译阶段。

一般模版的错误从头开始看,就容易找到了。

  • 22
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值