C++:模板

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库,也就是起源于模板的。

模板分为函数模板与类模板,我们先通过函数模板来了解模板的大部分规则:

函数模板

功能:

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

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

语法:

template <typename T1, typename T2 ......>
template <class T1, class T2 ......>

以上两种都是创建模板的方式,classtypename在里面是一样的功能,没有区别。在STL中多用class,所以本博客也以class为主。
而被classtypename定义是模板参数,它可以被任意类型代替。如果你不能理解,我们不妨看看示例。

示例:

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的类型?
编译器会根据调用函数时传入的参数,自动判断类型

比如以下调用:

Add(1, 5);
Add(3.0, 5.0);

对于Add(1, 5);,其两个参数都是int类型,那么此时模板就会生成一个int类型的Add函数。
对于Add(3.0, 5.0);,其两个参数都是double类型,此时模板就会生成一个double类型的Add函数。

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

那么我们现在来实现一下一开始swap函数的模板:

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

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


显式实例化

再回到刚刚的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);
Add<double> (a, b);

Add (a, b);此代码就是推演出int类型的函数;而Add (a, b);就是推演出double类型的函数。


模板参数缺省

在设置模板参数时,可以设置缺省值,在显式实例化时,对于没有指明的模板参数,会被推演为缺省值。

看到以下模板:

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

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

func<int, int>(8);

此时T1int类型,T2int类型,执行整数除法8/5=1

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

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);

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


参数匹配规则

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

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

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

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

那么我们调用函数时,会这么调用呢?

调用函数时,如果函数有现成的,完全匹配的函数,那么不会调用模板

如果我们这样调用函数:

Add(1, 2);

这个调用,两个参数都是int类型,而我们刚好写了一个两个参数都是int类型的函数,那么此次调用就不会调用模板,而是直接用我们写过的函数。

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

如果我们这样调用函数:

Add(1.0, 2.0);

此时两个参数都是double类型,如果不存在模板的话,double就会被转化为int,然后调用int,int的函数。但是由于模板存在,可以推演出更加匹配从函数double,doouble类型。所以此次调用会调用模板。


类模板

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

语法:

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

比如:

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

类名与类型

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

在一般的类中,类的类名和类型符号相同

比如stack的类,其类名为stack,类型也为stack

而在类模板中,不能单纯的将类名作为类型了,比如:

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

请问s1和s2的类型都是stack吗?
s1明明是用int推演的类,s2是用double推演的类,两者有很大的区别,如果都是stack类,后续如何区分?
所以我们用了其他规则来修饰这个类型符号,从而区分开同一个模板推演出来的不同类型。

类型 = 类名<模板参数>

对于stack s1;其类名为stack,类型为stack
对于stack s2;其类名为stack,类型为stack;


类成员的声明定义分离

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

假设我们希望分离析构函数~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,所以函数的声明应该这样写:

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

但是这还不是一个合法的声明。

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

也就是说要这样:

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

这才是一个模板类的成员函数声明。


非类型模板参数

我们的模板参数也可以不是一个类型,而是一个数值

对于指定类型的参数,我们称为类型形参,比如int,double。
对于一个数值的参数,我们称为非类型形参。

比如以下类模板:

template <class T, int N>
class Array
{
public:

private:
	T _arr[N];
};

我们在模板参数列表中有一个类T,一个int类型的N,此时T就是类型形参,N就是非类型形参。

在定义类时,可以通过这个非类型参数,为这个类传入一些值:

Array<int, 10> a;
Array<double, 20> b;

对于a这个对象,我们在创建时,为T传入了intN传入了10。那么经过初始化,_arr就指向了10int的数组。

对于b这个对象,我们在创建时,为T传入了doubleN传入了20,那么经过初始化,_arr就指向了20double类型的数组。

注意:非类型模板参数必须是整型,bool,char类型的常量


模板特化

通常情况下,使用模板可以实现一些与类型无关的代码,但是对于一些特殊的类型,有可能会得到错误的结果。

比如以下代码:

template<class T>
bool Less(T x, T y)
{
	return x < y;
}

int main()
{
	int* p1 = 5;
	int* p2 = 10;
	cout << Less(p1, p2) << endl;
	return 0;
}

这个函数模板中,我们用Less来比大小,我们此时传入了两个指针p1p2,原本的意图是通过指针来比较数字5和10的大小。但是当传入后,我们比较的是p1 < p2,也就是对两个指针比大小了,这不符合我们的预期。也就是说在面对指针的时候,我们需要特殊处理,这就需要模板特化了。

模板特化的功能就是:

在原模版的基础上,针对特殊类型进行特殊化的实现方式

其分为函数模板的特化与类模板的特化:


函数模板特化

我们先看到一个函数模板特化,再讲解语法:

//基础模板
template<class T>
bool Less(T x, T y)
{
	return x < y;
}

//模板特化
template<>
bool Less<int*>(int* x, int* y)
{
	return *x < *y;
}

第一段代码是一般的函数模板,而第二段是对int进行了特化的版本,当我们传入参数类型为int时,就会调用这个特化版本,执行*x < *y,先解引用再比较。

那么这个模板特化有什么特点呢?
首先,我们将T特化为了int*,所以T不再是一个需要推演的参数了,此时将T从模板参数列表中抽离出来,改为int*放到函数名Less后面,用尖括号括起来,然后把函数参数中所有的T改为特化后的int*

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

  1. 必须存在一个基础模板
  2. 对于特化版本,template后面的<>内部不写被特化的模板参数
  3. 对于特化版本,在函数名后跟上<>,内部指定特化类型
  4. 将特化前的模板参数改为特化后的具体类型

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

比如这样:

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

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

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


类模板特化

类模板特化的语法和刚才是一样的:
模板特化要满足以下语法:

  1. 必须存在一个基础模板
  2. 对于特化版本,template后面的<>内部不写被特化的模板参数
  3. 对于特化版本,在函数名后跟上<>,内部指定特化类型
  4. 将特化前的模板参数改为特化后的具体类型

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


全特化

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

比如以下案例:

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

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

比如:

Data<int, char> d1;

这里的d1就是一个模板特化创造出来的类对象。


偏特化

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

偏特化分为部分特化和限制特化:

部分特化

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

比如以下案例:

//基础模板
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;

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


限制特化

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

比如以下案例:

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

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

而这样对模板参数进行限制,就是限制特化了。


你可能感兴趣的:(C++,C++,开发语言)