Bootstrap

C++入门 模板(进阶)

目录

非类型模板参数

模板的特化

概念

函数模板特化

类模板特化

全特化

偏特化

模板分离编译

模板总结


非类型模板参数

在了解模板进阶之前,先看一个特性:例如我们要搞一个可以变化容量大小的栈,在C语言中是这样定义的:

#define N 100

// 静态的栈
template<class T>
class Stack
{
private:
	int _a[N];
	int _top;
};

int main()
{
	Stack<int> st1;		// 10
	Stack<int> st2;		// 100
	return 0;
}

通过修改宏的值来修改容量大小,在C++中我们不这样操作, 我们在模板上直接增加一个非类型模板参数。例如以下代码所示:

// 静态的栈
template<class T, size_t N = 10>
class Stack
{
public:
	void func()
	{
		// N是常量
		N++;
	}
private:
	int _a[N];
	int _top;
};

int main()
{
	Stack<int> st1;				// 10
	Stack<int, 100>  st2;		// 100
	Stack<int, 1000> st3;		// 1000

	// 按需实例化
	//st1.func();
	return 0;
}

以上的size_t N就是一个非类型模板参数(C++20之前,只允许整形做非类型模板参数,而C++20之后,可以支持double等其他内置类型)

为什么这里N++不调用func就不会报错呢?原因如下:编译器只会对模板大体有一个检验,它不会去查看类内容是否正确,只有在main函数中将它实例化后,才会发生编译错误。

类模板没实例化时,不去里面查细节东西,无法确认时类型还是静态变量,我们再浅看一下类类型形参,类类型形参就是跟在typename后面的参数: 

void PrintVector(const vector<int>& v)
{
	vector<int>::const_iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

template<class T>
void PrintVector(const vector<T>& v)
{
	// 加typename明确告诉是类型
	//typename vector<T>::const_iterator it = v.begin();
	auto it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

int main()
{
	vector<int> v1 = { 1,2,3,4,5,6,7 };
	vector<double> v2 { 1.1,2.2,3.3,4.4,5.5,6.6,7.7 };
	PrintVector(v1); //调用第一个
	PrintVector(v2); //调用第二个
	return 0;
}

简单介绍一下模板参数分为:类类型形参与非类型形参

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

非类型形参:就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。另外注意以下两点:

  1. 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
  2. 非类型的模板参数必须在编译期就能确认结果。

模板的特化

概念

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

// 函数模板 -- 参数匹配
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;
}

可以看到,Less绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果。上述示例中,p1指 向的d1显然小于p2指向的d2对象,但是Less内部并没有比较p1和p2指向的对象内容,而比较的是p1和p2指针的地址,这就无法达到预期而错误。

此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特 化中分为函数模板特化与类模板特化。


函数模板特化

函数模板的特化步骤:

  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()
{
	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;
}

如果是const修饰的变量就要注意const放的位置,如下代码所示:

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

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

在模板中const修饰的是变量,而不是指向的内容,所以特化后的模板也要修饰相同内容,如果放在Date*之前则表示指向的内容不可修改,而本身可修改,与原模版权限相违背。

注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。比如日期的比较,模板不如函数可读性高,能用函数尽量用函数

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

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


类模板特化

特化:针对某些特殊类型,进行特殊化处理。

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

可以看到T1,T2都被给了明确的类型,这就是全特化。


偏特化

偏特化:也叫半特化,任何针对模版参数进一步进行条件限制设计的特化版本。

偏特化有以下两种表现方式:

1、部分特化:将模板参数类表中的一部分参数特化。

// 偏特化/半特化
template <class T1>
class Data<T1, int>
{
public:
	Data() { cout << "Data<T1, int>-偏特化" << endl; }
private:
	T1 _d1;
	int _d2;
};

根据上述概念,我们用一些实例进行测试,进一步区分三者区别:

int main()
{
	Data<int, int> d1; //Data<T1, int>-偏特化
	Data<int, char> d2;//Data<int, char>-全特化
	Data<int, double> d3;//Data<T1, T2>-原模版
	return 0;
}

d1由于只有第二个参数符合偏特化,优先调用,d2两个参数类型都符合全特化,先调用准备好的,d3两个参数都与全特化和偏特化不同,只能调用原模板。

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

// 限定模版的类型
// 两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:
	Data() { 
		cout << typeid(T1).name() << endl;
		cout << typeid(T2).name() << endl;
		cout << "Data<T1*, T2*>" << endl << endl;
	}
};

//两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
	Data()
	{
		cout << typeid(T1).name() << endl;
		cout << typeid(T2).name() << endl;
		cout << "Data<T1&, T2&>" << endl << endl;
	}
private:
};

template <typename T1, typename T2>
class Data <T1&, T2*>
{
public:
	Data()
	{
		cout << typeid(T1).name() << endl;
		cout << typeid(T2).name() << endl;
		cout << "Data<T1&, T2*>" << endl << endl;
	}
private:
};

我们依旧用几个例子来看偏特化的区别:

int main()
{
	//int
	//double
	//Data<T1*, T2*>-偏特化
	Data<int*, double*> d4;
	//int
	//int* 
	//Data<T1*, T2*>-偏特化
	Data<int*, int**> d5;
	//int
	//int
	//Data<T1&, T2&>
	Data<int&, int&> d6;
	//int
	//int
	//Data<T1&, T2*>
	Data<int&, int*> d7;
	return 0;
}

虽然模板上为引用或指针,但是模板类型依旧为内置类型,原因是为了处理多种情况而不只是应对指针或者引用。

 例如我们要排序一个优先级队列,push的是每个元素的地址,单纯比较无法得出正确答案,因为比较的是元素的地址。这时候就要偏特化出一个指针类型,比较的是指针指向的内容。


模板分离编译

什么是分离编译?一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链 接起来形成单一的可执行文件的过程称为分离编译模式。简单理解就是.h和.cpp两个文件分别存放定义和声明。

假如有以下场景,模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:

// Func.h
#pragma once
#include<iostream>
using namespace std;

template<class T>
T Add(const T& left, const T& right);

void func();

// Func.cpp
#include"Func.h"

template<class T>
T Add(const T& left, const T& right)
{
	cout << "T Add(const T& left, const T& right)" << endl;
	return left + right;
}

void func()
{
	cout << "void func()" << endl;
}

以上声明和定义分离,如果我们要调用add函数模板能够成功吗?

#include"Func.h"

int main()
{
	// 函数模版链接找不到
	Add(1, 2);     // call Z3Addii()
	Add(1.0, 2.0); // call Z3Adddd()
	Add('x', 'y'); // call Z3Addcc()


	// 普通函数可以链接找到
	func();

	return 0;
}

可以看到我们无法使用函数模板,原因有以下几点:

由于模板没有被实例化,add去.h文件中查找,虽然有声明但是没有定义,不知道如何实例化,而.cpp中有定义,但是main函数不会进里面找,不知道实例化成什么,但是func函数由于不要实例化,直接生成地址放在符号表里,需要调用时直接使用即可。 

当然应对这种情况也有解决办法,最佳解决方案:不要分离到两个文件,写到一个文件(原理:调用的地方,就有定义,就直接实例化)比如以上的add函数模板,我们把声明和定义都放在.h文件中。

template<class T>
T Add(const T& left, const T& right)
{
	cout << "T Add(const T& left, const T& right)" << endl;
	return left + right;
}

如果是类模板,长的成员函数,声明和定义分离,写到当前文件夹外面,短的直接定义在类里面(声明和定义依旧保证在同一个文件里.h)

另外一种方法是将模板定义的位置显式实例化。这种方法不实用,不推荐使用

// Func.cpp
#include"Func.h"

template<class T>
T Add(const T& left, const T& right)
{
	cout << "T Add(const T& left, const T& right)" << endl;
	return left + right;
}

// 显示实例化,这种解决方式很被动,需要不断添加显示实例化
template
int Add(const int& left, const int& right);

template
double Add(const double& left, const double& right);

void func()
{
	cout << "void func()" << endl;
}

模板总结

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