C++ | 泛型编程,函数模板,类型模板,非类型模板

C++模板

  • 问题引入
  • 1函数模板
    • 1.1显式实例化
    • 1.2参数匹配规则
  • 2类模板
    • 2.2类名与类型
    • 2.3类成员的声明定义分离
  • 3.非类型模板参数
  • 4. 模板的特化
    • 4.1 概念
    • 4.2 函数模板特化
    • 5.类模板特化
    • 5.1全特化
    • 5.2偏特化
      • 5.2.1部分特化
      • 5.2.2限制特化

问题引入

我们在C++中如何实现一个通用的swap交换函数?

或许你可以这样:

void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
void Swap(double& left, double& right)
{
double temp = left;
left = right;
right = temp;
}
void Swap(char& left, char& right)
{
char temp = left;
left = right;
right = temp;
}
......

  • 把每种类型都进行重载,写出n多种交换函数 ,但这很明显是一个费力的方法,不仅会让代码冗余重复,而且写出这样的代码,也会耗费精力。

  • 那么有没有一种办法,我们给编译器一个模板,编译器自动生成函数?
    C++就样提供了模板,而C++最重要的STL库,也就是起源于模板的。
    模板分为函数模板与类模板,我们先通过函数模板来了解模板的大部分规则:

1函数模板

功能:

函数模板代表了一个函数族,在使用时被参数化,根据实参的类型生成特定类型的版本

也就是说,我们可以通过一个函数模板,让编译器生成一整个同类型的函数家族。

语法:

template <typename T1, typename T2 ......>
template <class T1, class T2 ......>
  • 以上两种都是创建模板的方式,class和typename在里面是一样的功能,没有区别。
  • 而被class和typename定义是模板参数,它可以被任意类型代替。如果你不能理解,我们不妨看看示例。
template <class T>
T Add(T x, T y)
{
	return x + y;
}

以上就是一个函数模板,T是一个模板参数,它代表一个类型。如果T是int,那么以上函数就是:

int Add(int x, int y)
{
	return x + y;
}

这个函数是不是很熟悉了?
也就是说,T是可以被替换的类型,那么我们要如何确定这个T的类型?

  • 编译器会根据调用函数时传入的参数,自动判断类型。
  • 以此类推,我们不论想要多少种类型,只需要传入参数,让编译器自动识别,而我们只需要写一个模板,就可以衍生出无数种函数,这就是模板的优势。

让我们利用模板特点实现一下最初说的通用swap函数:

template <class T>
void Swap(T& left, T& right)
{
	T temp = left;
	left = right;
	right = temp;
}

不过要注意,模板只是一个蓝图,本身不是函数,当我们传入指定类型参数,其就会生成相应的函数。

1.1显式实例化

再回到刚刚的Add函数模板。

template <class T>
T Add(T x, T y)
{
	return x + y;
}

如果我们传入两个不同类型的参数怎么办?
比如这样:

int a = 5;
double b = 3.0;
Add(a, b);
  • 请问这个模板是转化为double类型好呢,还是转化为int类型好呢?
    对编译器来说,这就是一个大问题了,如果转化错误了,编译器就要背黑锅。所以遇到这种情况,编译器不会为我们做决定,而是报错,必须由程序员指明要用哪一种类型的模板。

比如使用强制类型转换:

int a = 5;
double b = 3.0;
Add(a, (int)b);
Add((double)a, b);

上述代码,Add(a, (int)b);将b转化为了int,此时模板推演出int类型的函数;而Add((double)a, b);a转化为了double类型,此时模板推演出double类型的参数。

此外,我们还可以使用显式实例化的方式:
显式实例化,就是在使用模板时,明确的告诉模板,要用什么类型。

语法:

函数名 <类型> (参数);
int a = 5;
double b = 3.0;
//显式实例化
Add<int> (a, b);//推演出int类型的函数
Add<double> (a, b);//推演出double类型的函数
  • 模板参数缺省
    在设置模板参数时,可以设置缺省值,在显式实例化时,对于没有指明的模板参数,会被推演为缺省值。
template <class T1, class T2>
void func(T1 a)
{
	T2 b = 5;
	cout << a / b;
}

这个模板中,函数func只有一个形参,而T2这个模板参数不在形参中,而是用于定义b这个变量了。此时T2是无法根据函数的实参推演出来的,必须显式实例化中指明。
比如这样:

  • 此时T1为int类型,T2为int类型,执行整数除法8/5=1
func<int, int>(8);

接下来我们给模板参数缺省值试试:

						//T2缺省值为double
template <class T1, class T2 = double>
void func(T1 a)
{
	T2 b = 5;
	cout << a / b;
}

此处我们给了T2一个缺省值double,也就是说我们不传入第二个模板参数,T2就会被推演为缺省值double。

func<int>(8);
func(8);
  • 对于fun< int>(8),此时T2就会被推演为缺省值double,变量b就是double类型了,此时执行小数除法8 / 5.0 = 1.6。
    对于func(8),我们没有进行显式实例化,此时对于T1,由于我们传入了参数a为int类型,此时T1被推演为int,而T2得到缺省值double,执行小数除法8 / 5.0 = 1.6。

1.2参数匹配规则

模板本身不是一个函数,所以同名的函数和模板是可以共存的。
比如这样:

template <class T>
T func(T x, T y)
{
	return x + y;
}

int func(int x, int y)
{
	return x + y;
}

以上代码中我们创建了一个Add的模板,一个Add的int类型函数。

  • 那么我们调用函数时,会如何调用呢?

  • 调用函数时,如果函数有现成的,完全匹配的函数,那么不会调用模板,
    调用函数时,如果可以通过模板产生更加匹配的函数,那么会调用模板进行推演

Add(1, 2);//调用函数
Add(1.0,2.0);//调用模板

2类模板

类模板的特性与函数模板几乎一致,此处只讲解类模板的特殊的地方。

语法:

template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};

先看一个简单的示例:

template <class T>
class stack
{
public:
	stack(size_t capacity = 10)
	:_pData(new T[capacity])
	,_size(0)
	,_capacity(capacity)
	{}
	
private:
	T* _pData;
	size_t _size;
	size_t _capacity;
};

  • 这就是一个stack类的模板,有了这个模板,我们的栈就可以存放int,double等等的其他类型了。
  • 我们的模板参数为T,由于类没有传参的概念,不能通过参数来推演类型,所以一般而言类的模板都是要显式实例化的。

比如:

stack<int> s1;
stack<double> s2;

2.2类名与类型

通过模板创建的类,其类名与类型也有所不同,接下来我们看看规则:

在一般的类中,类的类名和类型符号相同
而在类模板中,不能单纯的将类名作为类型了

stack<int> s1;
stack<double> s2;
//对于stack s1;其类名为stack,类型为stack
//对于stack s2;其类名为stack,类型为stack

2.3类成员的声明定义分离

当我们希望把一些类中的成员定义在类的外部时,那就需要声明和定义分离。

假设我们希望分离析构函数~stack。
对于一般的类,我们会这样分离:

class stack
{
public:
	stack(size_t capacity = 10)
	:_pData(new int[capacity])
	,_size(0)
	,_capacity(capacity)
	{}
	
	~stack();//声明
	
private:
	int* _pData;
	size_t _size;
	size_t _capacity;
};

stack::~stack()
{
	//函数体
}

  • 首先要用类型::函数名来限定作用域,然后再开始定义函数。

  • 对于类模板也要类型::函数名来限定作用域。类模板的类型刚刚介绍过,就是stack,所以函数的声明应该这样写:

对于类模板,当在类外定义函数时,要添加模板参数列表。

template <class T>
stack<T>::~stack()
{
	//函数体
}


3.非类型模板参数

  • 模板参数分类类型形参与非类型形参
  • 类型形参:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
  • 非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用
namespace bite
{
// 定义一个模板类型的静态数组
template<class T, size_t N = 10>
class array
{
public:
T& operator[](size_t index){return _array[index];}
const T& operator[](size_t index)const{return _array[index];}
size_t size()const{return _size;}
bool empty()const{return 0 == _size;}
private:
T _array[N];
size_t _size;
};
}
  • 非类型模板参数必须是整型,bool,char类型的常量

注意:

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

4. 模板的特化

4.1 概念

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

// 函数模板 -- 参数匹配
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指针的地址,这就无法达到预期而错误。
  • 此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化。

4.2 函数模板特化

函数模板的特化步骤:

  1. 必须要先有一个基础的函数模板
  2. 关键字template后面接一对空的尖括号<>
  3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
  4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
// 函数模板 -- 参数匹配
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;
}

但是函数模板是一个没有必要的东西,因为相比于对模板进行特化,不如直接重载一个函数,模板特化在类模板中较为有用。

比如这样:

bool Less(int* x, int* y)
{
	return *x < *y;
}

可以达到一样的效果,而且无需繁杂的语法。

模板特化的主要用处体现在类模板特化上,函数并不推荐使用这个模板特化。

5.类模板特化

类模板特化的语法和刚才是一样的:

  • 模板特化要满足以下语法:

  • 必须存在一个基础模板

  • 对于特化版本,template后面的<>内部不写被特化的模板参数

  • 对于特化版本,在函数名后跟上<>,内部指定特化类型

  • 将特化前的模板参数改为特化后的具体类型

  • 类模板特化分为全特化和偏特化

5.1全特化

全特化是指将模板参数的所有参数都确定下来

比如以下案例:

//基础模板
template<class T1, class T2>
class Data
{
public:
	Data() {cout<<"Data" <<endl;}	
private:
	T1 _d1;
	T2 _d2;
};

//模板特化
template<>
class Data<int, char>
{
public:
	Data() {cout<<"Data" <<endl;}
private:
	int _d1;
	char _d2;
};

此处我们将T1,T2两个参数都设立了特化,这就叫做全特化。
只有模板参数第一个值为int,第二个参数为char,调用此类。

Data<int, char> d1;
//输出Data,说明是模板特化造出来的类对象

5.2偏特化

偏特化是指并没有把模板参数确定下来,但是对满足特定条件的模板参数,执行特化版本
偏特化又分为部分特化和限制特化

  • 部分特化只是将一部分参数特化

5.2.1部分特化

//基础模板
template<class T1, class T2>
class Data
{
public:
	Data() {cout<<"Data" <<endl;}	
private:
	T1 _d1;
	T2 _d2;
};

//模板特化
template<class T1>
class Data<T1, char>
{
public:
	Data() {cout<<"Data" <<endl;}
private:
	T1 _d1;
	char _d2;
};

以上的第二段代码就是一个部分特化,其只特化了第二个模板参数为char,只有当第二个参数为char类型,不论第一个参数类型是什么,都会调用特化版本的类了

由于T1没有被确定下来,仍然需要推演,所以第一行的模板参数列表保留T1。

比如:

Data<int, char> d2;
Data<double, char> d3;

这里的d2,d3都是通过模板特化创建出来的对象,因为它们满足第二个模板参数是char类型。


5.2.2限制特化

限制特化是对参数进行条件限制,但是没有把参数类型确定下来

//基础模板
template<class T1, class T2>
class Data
{
public:
	Data() {cout<<"Data" <<endl;}	
private:
	T1 _d1;
	T2 _d2;
};

//模板特化
template<class T1, class T2>
class Data<T1*, T2*>
{
public:
	Data() {cout<<"Data" <<endl;}
private:
	T1 _d1;
	T2 _d2;
};

  • 此处是限定:当T1,T2为指针是,调用此模板特化。
  • 也就是说,这个过程中,T1,T2的类型是不确定的,任然需要推演,所以第一行的模板参数列表保留了T1,T2。
  • 而这样对模板参数进行限制,就是限制特化了。

你可能感兴趣的:(C++,c++,数据结构,算法)