文章目录
1. 泛型编程
还记得我们经常使用的swap函数的实现吗?
void swap(int& a,int& b)
{
int tmp = a;
a = b;
b = tmp;
}
很显然,这个函数只针对于int类型元素之间的交换有效,即swap(1,2)这种的,但是我要实现swap(1.0,2.0)的呢?实现swap(“abc”,“cdsa”)这种字符串类型的呢?该怎么解决。在没学习模板之前,我们可能会首先想到函数重载。
void swap(double& a,double& b)
{
double tmp = a;
a = b;
b = tmp;
}
void swap(string& a,string& b)
{
string tmp = a;
a = b;
b = tmp;
}
使用函数重载虽然可以实现,但是有一下几个不好的地方:
- 重载的函数仅仅只是类型不同,代码的复用率比较低,只要有新类型出现时,就需要增加对应的函数
- 代码的可维护性比较低,一个出错可能所有的重载均出错
那么还有更好的办法吗?答案肯定是有,就是我们本节讲的模板。
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
模板它实现了通用,即可以将类型进行参数化。
其中,模板又分为函数模板和类模板。
2. 函数模板
2.1 概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
2.2 格式
template<typename T1, typename T2,…,typename Tn>
返回值类型 函数名(参数列表){}
就用刚刚说的swap函数来举个例子:
template<typename T>
void swap(T& a, T& b)
{
T tmp = a;
a = b;
b = a;
}
那么,我们在交换int类型的元素时,直接swap(1,2)即可,交换double类型的元素时,直接swap(1.0,2.0)等等。
2.3 原理
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。(即就是将重载的工作交给了编译器),因为会生成对应的代码,所以其效率和同时调用三个不同的swap函数一样
根据我们输入的参数类型,编译器会进行推演并生成对应类型的函数以供调用。、
2.4 实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。函数模板会实例化出一个模板函数出来。
2.4.1 隐式实例化
隐式实例化:让编译器根据实参推演模板参数的实际类型
还是上面的swap函数,当我们直接调用swap(1,1)
或swap(1.0,2.0)
的时候,编译器会根据实参的类型自动推演出模板参数所对应的实际的类型。
但当调用的两个元素是不同的元素的时候,就会出现错误,如swap(1,2.0)
。
原因:
这样会产生参数的二义性。当编译器进行编译的时候,会发现将T推演为int类型,或是将T推演为double类型都是可以的,那么就无法确定到底需要推演成为哪个类型,就会发生错误。
解决:
- 让用户自己来进行强制转换,如
swap((double)1,2.0);
- 使用模板的显示的实例化,如
swap<int>(1,2.0);
- 对swap函数再添加一个模板参数,如下:
template <typename T1, typename T2> void swap(T1& a,T2& b) { T1 tmp = a; a = (T1)b; b = tmp; }
这样调用
swap(1,2.0)
也是可以的,因为它解决了参数二义性的问题
2.4.2 显示实例化
显式实例化:在函数名后的<>中指定模板参数的实际类型
在上面解决不同类型元素调用模板所造成参数二义性问题时,我们就已经用到了模板的显示实例化,即swap<int>(1,2.0);
如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
碎片知识:可以使用typeid(type).name()
语句,打印type元素的类型。
2.5 最佳匹配原则
- 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板。
- 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
2.6 函数模板和模板函数
-
模板函数是函数模板实例化时生成的。
-
函数模板并不是函数,而是编译器使用特定的方式生成的函数的模具
可以对比指针数组和数组指针的概念,函数模板,首先它是一个模板,然后才是函数;而模板函数,首先它是一个函数,其次才是模板。
3. 类模板
类模板和函数模板的概念一样,这里就不过多介绍。
3.1 格式
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
3.2 实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
对于类模板而言,只有那些被调用的成员函数才会被实例化。
3.3 非类型模板参数
对于函数模板和类模板,模板参数并不局限于类型,普通值也可以作为模板参数。
在基于类型参数的模板中,你定义了一些具体细节未加确定的代码,直到代码被调用时这些细节才被真正确定。然而,在这里,我们面对的这些细节是值(value), 而不是类型。
当要使用基于值的模板时,你必须显式地指定这些值,才能够对模板进行实例化,并获得最终代码。
例如:
非类型的类模板参数:
//可以给该模板参数指定缺省值
template <class Type, int sz = 8>
class Stack{
private:
T elems[sz];
//...
};
非类型的函数模板参数:
template <typename T,int val>
T addValue(T const& x)
{
return x + val;
}
我们还应该知道,非类型模板参数是有限制的。通常而言,它们可以是常整数(包括枚举类型)或者是指向外部链接对象的指针。但是,浮点数和类对象是不允许作为非类型模板参数的;
之所以不能使用浮点数(包括简单的常量浮点表达式)作为模板实参是有历史原因的。然而,该特性的实现并不存在很大的技术障碍;因此,将来的C++版本可能会支持这个特性。
3.4 零初始化
对于int. char 或者指针等基本类型,并不存在 “用一个有用的缺省值来对它们进行初始化” 的缺省构造函数;相反,任何未被初始化的局部变量都具有一个不确定(undefined) 值;
因此,我们需要将其用缺省值进行初始化,例如调用int()
我们将获得0,调用char()
将获得’\0’,对于一个类对象或者结构体对象也是如此,我们可以直接调用Test()
来对Test t
对象 进行初始化,并且,该Test()对应的值是我们定义的缺省的构造函数,如:Test() : m_data() { }
。
3.5 typename和class
在C++语言的演化过程中,关键字typename的出现相对较晚一些; 在它之前,关键字class 是引入类型参数的唯一方式, 并一直作为有效方式保留下来。
从语义上讲,这里的class和typename是等价的。因此,即使在这里使用了class,你也可以用任何类型(前提是该类型提供模板使用的操作)来实例化模板参数。
然而,class 的这种用法往往会给人误导(这里的class并不意味着只有类才能被用来替代T,事实上基本类型也可以);因此对于引入类型参数的这种用法,你应该尽量使用typename。另外还应该注意,这种用法和类的类型声明不同,也就是说,在声明(引入)类型参数的时候,不能用关键字struct代替typename。
但是typedname和class在某些地方是独一无二的,互相之间不能替代。
3.5.1 只能使用typename的例子
在C++标准化过程中,引入关键字typename是为了说明:模板内部的标识符可以是一个类型。譬如下面的例子:
template <typename T>
class MyClass{
typename T :: SubType* ptr;
//...
};
上面的程序中,第二个typename被用来说明:SubType是定义于类T内部的一种类型,因此,ptr是一个指向T : : SubType类型的指针。
如果不使用typename,SubType就会被认为是一个静态的成员,那么它应该是一个具体的变量或对象,那么T :: SubType * ptr
就会被看做是类T的静态成员SubType和ptr的乘积。
总结:通常而言,当某个依赖于模板参数的名称是一个类型时,就应该使用typename。
3.5.2 只能使用class的例子
我们首先需要了解一下,模板的模板参数,或者说类模板也可以作为模板参数,我们称之为模板的模板参数
这里使用Stack来进行举例,在一个类Stack中,如果要使用一个和缺省值不同的内部容器,我们必须两次指定元素类型,也就是说,为了指定内部容器的类型,我们需要同时传递容器的类型和它所含元素的类型,如下:
Stack<int,vector<int> > vStack;
然而,借助于模板的模板参数,我们可以只指定容器的类型而不需要指定所含元素的类型就可以声明这个Stack模板:Stack<int, vector> vStack;
为了获得这个特性,我们必须把第二个模板参数指定为模板的模板参数。那么Stack的部分声明如下:
template < typename T, template<typename ELEM> class CONT = deque > //deque为一个队列
class Stack{
private:
CONT<T> elems;
//...
};
该Stack的声明中第二个模板参数现在是被声明为一个类模板,在使用时,第二个参数必须是一个类模板,并且由第一个模板参数传递进来的类型进行实例化,这也是一个比较特别的地方。一般的,我们可以使用类模板内部任意的任何类型来实例化模板的模板参数。
而我们也前面提过:作为模板参数的声明,通常可以使用typename来替换关键字class。然而,上面的CONT是为了定义一个类,因此只能使用关键字class。因此,一个错误的例子如下:
template < typename T, template<typename ELEM> typename CONT = deque >
class Stack
{
//... 是错误的
};
总结:当一个模板参数中含有模板的模板参数(类模板),就只能使用class来标识。
本节的内容节选自书籍《C++ Templates》中第五章的第四小节
4. 模板特化
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,比如当我们定义了一个函数模板来实现对两个数进行比较的,如果我们传入的是int类型、double类型或者是char类均是可以的,但若是我们传入的是一个char类型的指针呢?
这个时候就需要对该模板进行特化,即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。话句话来说,特化就是根据泛化的模板来处理某种特殊化情况的实现方式,模板特化中分为函数模板特化与类模板特化。
4.1 函数模板特化
函数模板特化步骤:
- 必须要先有一个基础的函数模板
- 关键字template后面接一对空的尖括号<>
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型
- 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
例如:
template<typename T>
bool isequal(T a,T b)
{//.......}
//函数模板特化
template<>
bool isequal(char* a,char* b)
{//......}
一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。
4.2 类特化
类特化又分为两种:全特化和偏特化
① 全特化
全特化即是将模板参数列表中所有的参数都确定化。
template<class T1,class T2>
class Test{
public:
//.....
private:
T1 m_a;
T2 m_b;
};
//全特化
template<>
class Test<int ,char>{
public:
//.....
private:
T1 m_a;
T2 m_b;
};
② 偏特化
偏特化是对任何针对模版参数进一步进行条件限制设计的特化版本。
偏特化也分为两种表现形式:部分特化和参数更进一步限制
部分特化:将模板参数类表中的一部分参数特化。
//偏特化----部分特化
template<class T1>
class Test<T1,char>{
public:
//.....
private:
T1 m_a;
char m_b;
};
参数更进一步限制:针对模板参数更进一步的条件限制所设计出来的一个特化版本。
//偏特化----参数更进一步限制
template<class T1,class T2>
class Test<T1* ,T2* >{
public:
//.....
private:
T1 m_a;
T2 m_b;
};