c++中的template的进阶用法

欢迎来到博主的专栏:c++杂谈
博主ID:代码小豪


非类型模板参数

常见的模板类或者模板函数(后面统称模板),其模板参数都是类型。比如STL中的vector。

template < class T, class Alloc = allocator<T> > class vector; 

模板类是不能用作对象的类型声明的,必须传入参数实例化出相应的类才能用作类类型。

void TestVector()
{
	vector<int> v1;//传入给T的参数是int
	vector<string>v2;//传入给T的参数是string
}

这种需要传入类型来进行实例化模板的参数成为类型模板参数,我们先来看看下面这么几种模板用法

template<class T, size_t N>
class stack
{
private:
	T arr[N];
	size_t top = 0;
	size_t capacity = N;
};

这是一个模板类stack,其中T是模板类型参数,N是非类型模板参数,类型模板参数代表一个类型,而非类型模板参数表示一个值。我们通过一个具体的类型名而非class,typename来定义一个非类型模板参数。

当模板被实例化时,非类型模板参数会被用户提供的值来替代,且这个值是一个常量,不能修改。

	stack<int,10>s1;//可容纳10元素的栈
	stack<int,100> s2;//可容纳100元素的栈

要注意的是,虽然实例化出的类的元素类型都是相同的,即s1和s2的栈元素都是int类型,但是这两个对象的类型却不相同,可以用typeid来查看s1和s2的类型。

	cout << typeid(s1).name() << endl;//class test::stack<int,10>
	cout << typeid(s2).name() << endl;//class test::stack<int,100>

非类型模板参数也可用于声明模板函数,其作用与模板类一致。

template<size_t N1,size_t N2>
void func1(const char*(&arr1)[N1],const char*(&arr2)[N2])
{
	cout << N1 << ' ' << N2 << endl;
}

但是和类模板存在一点不同,那就是模板函数可以自动推导非类型模板参数的值,不一定必须显示实例化。

func1("hello world", "wwwwwwwww");
//这个函数会根据传入数组的元素大小实例化N1,N2

STL中也存在非类型模板参数的模板容器。比如c++11标准推出的array容器

template < class T, size_t N > class array;

缺省模板参数

和函数的参数的缺省形式类似,模板也可以为参数设立一个缺省值。这一特性经常用于STL当中。比如基本上STL中的所有容器都会缺省掉内存池的类型。即默认使用STL库中的内存配置器

template < class T, class Alloc = allocator<T> > class vector;
//缺省了配置器类型 
template <class T, class Container = deque<T> > class stack;
//缺省了容器类型

如果我们在实例化模板类的时候不修改缺省模板参数,就会使用默认的模板参数,比如

template<class T=int>
	class calcular
	{
	public:
		T add(const T& a, const T& b)
		{
			return a + b;
		}
	};

如果不在类模板中传递参数,那么T的参数会是int,如果传入其他参数,编译器会将模板会将T实例化成其他类型。

void Testtemplate2()
	{
		calcular<> cal1;//默认int
		calcular<string> cal2;//计算string
		string str1("hello");
		string str2("world");
		cout<<cal1.add(10, 20)<<endl;//30
		cout<<cal2.add(str1,str2) << endl;//helloworld
	}

typename和class的区别

如果typename和class出现在模板的参数声明上,那么class和typename没有任何区别

	template<class T, size_t N> class stacktemplate<typename T, size_t N> class stack//这两种模板类的声明都没有问题

但是typename和class并非永远等价,在下面的一种场景中我们必须使用typename。那就是在模板类中嵌套使用另外一个模板类的类内声明:比如

	template<class T>
	void func(const vector<T>& v)
	{
	//嵌套使用了定义在模板类中的类型iterator
		vector<T>::const_iterator begin = v.begin();
		while (begin != v.end())
		{
			cout << *begin << ' ';
			begin++;
		}
	}

如果你尝试运行这段代码,会发现一个看起来有点莫名其妙的报错

错误 C3878 语法错误: “expression” 后出现意外标记 “标识符”

为什么这个程序会发生这样的错误呢?我们还得要从模板类的编译规则说起,首先,模板类的编译和普通类的编译可不一样,和普通类严格的类型判断不同,模板类则是当模板被实例化的时候,编译器才会对模板类进行检查,这一点可以在很多例子上得到验证。

我们可以将模板当中的类型分为三种,一种是从属类型(dependent name),即和模板参数T有关的类型,比如上面的vector<T>,这种类型在实例化的时候就会把T替换成相应实参,当模板实例化后,这种类型不会引发编译器解析错误,第二种是非从属类型,即int等内置类型,以及非模板类,这些也不会引发编译器解析错误。

第三种则是嵌套从属名称,即这个类型不仅与T相关,而且还和其他的模板相关。比如vector<T>::iterator,这个类型就是vector模板中的类型,而且嵌套在了模板函数func当中。

那么为什么嵌套从属类型会引发编译错误呢?这就得提到编译器对于模板的处理规则了。目前已知的是,编译器不会对模板进行严格的检查,只有当模板实例化后才能检查语法问题,但是这个func中存在几个模板呢?一个是目前已知的func,还有一个则是STL中的vector,而func实例化很简单,只需要将实参替换T即可,但是vector的实例化适合func同时进行的吗?未必

当实例化func时,vector<T>变成了vector<int>,但是这并不意味着vector已经被实例化了,也就是说当编译器解析func时,尚未清楚vector到底是什么东西,那么当然也就无法确认vector<T>::iterator是什么东西了。它可能是一个类型,也可能是static成员。这就会导致歧义出现,编译器无法判断,因此解决方法就是,在嵌套从属类型前面加上typename,告诉编译器这就是一个类型,于是编译器就不会爆出语法错误了。

	template<class T>
	void func(const vector<T>& v)
	{
	//vector<T>::iterator前加上typename
		typename vector<T>::const_iterator begin = v.begin();
		while (begin != v.end())
		{
			cout << *begin << ' ';
			begin++;
		}
	}

模板特化

函数模板的特化

模板并非适合所有的类型,除去哪些行为不符合模板定义的类(比如缺少operator++的类就不能调用上述的cacl类成员函数add)。实际上还有一种情况如下,那就是对象符合模板的行为,但是不符合模板的逻辑。

比如我们定义一个比较元素的函数模板compare。

template<class T>
bool compare(const T& e1, const T& e2)
{
	return e1 < e2;
}

这个模板类基本适用于一切定义了operator<行为的类型(包括内置类型)。

cout<<compare<int>(1, 2);//1
cout<<compare<double>(1.0, 2.0);//1
cout<<compare<string>("haha", "xixi");//1

但是有没有某类,定义了<的行为的类型,但是不能正确符合compare函数的逻辑的类型呢?有,那便是指针类型。

我们可以尝试传递两个string*的参数到compare中,看看这个函数的运行情况

string* s1 = new string("haha");
string* s2 = new string("xixi");
cout<<compare(s1, s2);

多次运行可以发现,结果有时候是0,有时候1,这是因为compare被模板实例化成了compare<string*>,导致这个函数进行的是指针之间的比较,而非指针指向的内容的比较,为了让compare能够做到对指针的元素进行比较,我们就可以对compare模板进行特化。

template<>
bool compare<string*>(string* const& e1, string* const& e2)
{
	return *e1 < *e2;
}

特化的规则如下:

  1. 为了声明compare是一个模板函数,因此在compare的前面加上template
  2. 由于特化的版本不再需要类型参数,因此template的尖括号(<>)不加上任何的参数
  3. 在compare的后面要加上特化的参数类型,如<string*>
  4. 要将函数中的T替换成特化的参数类型

此时再次传入string指针到compare当中,就会发现compare的逻辑对于string*的参数会有不一样的逻辑。

string* s1 = new string("haha");
string* s2 = new string("xixi");
cout<<compare(s1, s2);//1

特化有一个特别需要关注的东西就是,特化版本的函数参数,必须符合模板中实例化出的参数。比如下面的声明就是不允许的

template<>
bool compare<string*>(int* const& e1, string* const& e2)

这是因为特化的版本的声明,必须符合原模板中被实例化的版本的声明。也就是说compare<string*>的函数声明,必须和compare<T>被实例化成compare<string*>的版本一致。这也就是为什么compare<string*>的类型声明这么奇怪的原因。

我们先来对比一下compare模板和偏特化compare模板的声明。

template<class T>
bool compare(const T& e1, const T& e2)template<>
bool compare<string*>(string* const& e1, string* const& e2)

有没有发现,const的位置好像往后移动了,这是因为compare<string*>的特化版本的声明要与compare<T>的实例化compare<string*>一致,在compare模板中,const修饰的是e1本身,那么在特化版本中,const也要修饰e1本身,也就是修饰指针本身,所以要放在*之后。

类模板的特化

类模板有两种特化形式,一种是全特化,一种是偏特化。

  1. 全特化:原模板参数全部被实际参数替代了,即特化版本是template<>,函数模板只支持全特化
  2. 偏特化:原模板中的部分参数实际参数替代了。这个在后面细说

类模板的全特化和函数模板的全特化性质类似,我们用下面几个代码测试出全特化的效果。

	template<class T1,class T2>
	class test
	{
	public:
		test()
		{
			cout << "test<T1,T2>,原模板" << endl;
		}
	};

	template<>
	class test<int, int>
	{
	public:
		test()
		{
			cout << "test<int,int>,全特化" << endl;
		}
	};

test<int>是test模板的全特化,在类的构造函数中输出不一样的信息,可以看到不同的实例化场景下,类的不同。比如:

test<int, double> t1;//调用原模板版本的构造函数
test<int, int> t2;//调用全特化版本的构造函数

偏特化则是再保留原模板的类型参数的基础上,根据不同的模板参数实例化出不一样的类。

template<class T>
class test<int, T>
{
public:
	test()
	{
		cout << "test<int,T>,偏特化" << endl;
	}
};

如果模板的实例化场景符合偏特化的形式,就会根据偏特化中的内容实例化出相应的类。

test<int,double> t3;//偏特化
test<int,float> t4;//偏特化
test<int, string>t5;//偏特化

还有一种偏特化是限定类型的形式,而非某种类型。
比如:

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

如果实例化的形式是某种类型的指针形式,就会实例化出这个偏特化的类。

test<int*, double*>t6;//偏特化
test<string*, float*> t7;//偏特化
  • 25
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

代码小豪

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

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

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

打赏作者

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

抵扣说明:

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

余额充值