1 | pear config-set http_proxy "" |
C++ 模板的显式具体化
C++ 没有办法限制类型参数的范围,我们可以使用任意一种类型来实例化模板。但是模板中的语句(函数体或者类体)不一定就能适应所有的类型,可能会有个别的类型没有意义,或者会导致语法错误。
例如有下面的函数模板,它用来获取两个变量中较大的一个:
1 | template<class T> const T& Max(cosnt T& a, const T& b) { |
请读者注意 a > b
这条语句,>
能够用来比较 int、float、char
等基本类型数据的大小,但是却不能用来比较结构体变量、对象以及数组的大小,因为我们并没有针对结构体、类和数组重载
>
。
另外,该函数模板虽然可以用于指针,但比较的是地址大小,而不是指针指向的数据,所以也没有现实的意义。
除了 >
,+
-
*
/
等运算符也只能用于基本类型,不能用于结构体、类、数组等复杂类型。总之,编写的函数模板很可能无法处理某些类型,我们必须对这些类型进行单独处理。
模板是一种泛型技术,它能接受的类型是宽泛的、没有限制的,并且对这些类型使用的算法都是一样的(函数体或类体一样)。但是现在我们希望改变这种 "游戏规则",让模板能够针对某种具体的类型使用不同的算法(函数体或类体不同),这在 C++ 中是可以做到的,这种技术称为模板的显式具体化(Explicit Specialization)。
函数模板和类模板都可以显式具体化,下面我们线讲解函数模板的显式具体化,再讲解类模板的显式具体化。
函数模板的显式具体化
在讲解函数模板的显式具体化语法之前,我们先来看一个显式具体化的例子:
1 | #include<iostream> |
运行结果:
1 | 20 |
本例中,STU 结构体用来表示一名学生(Student),它有三个成员,分别是姓名(name)、年龄(age)、成绩(score);Max() 函数用来获取两份数据中较大的一份。
要想获取两份数据中较大的一份,必然会涉及到对两份数据的比较。对于 int、float、char 等基本类型的数据,直接比较它们本身的值即可,而对于 STU 类型的数据,直接比较它们本身的值不但有语法错误,而且毫无意义,这就要求我们设计一套不同的比较方案,从语法和逻辑上都能行得通,所以本例中我们比较的是两名学生的成绩(score)。
不同的比较方案最终导致了算法(函数体)的不同,我们不得不借助模板的显式具体化技术对 STU 类型进行单独处理。第 14 行代码就是显式具体化的声明,第 34 行代码进行了定义。
请读者注意第 34 行代码, Max<STU>
中的
STU
表明了要将类型参数 T 具体化为 STU 类型,原来使用 T
的位置都应该使用 STU
替换,包括返回值类型、形参类型、局部变量的类型。
Max 只有一个类型参数 T,并且已经被具体化为 STU
了,这样整个模板就不再有类型参数了,类型参数列表也就为空了,所以模板头应该写做
template<>
。
另外,Max<STU>
中的 STU
是可选的,因为函数的形参已经表明,这是 STU
类型的一个具体化,编译器能够逆推出 T
的具体类型。简化后的函数声明为:
1 | template<> const STU& Max(const STU& a, const STU& b); |
函数的调用规则:
对于给定的函数名,可以有非模板函数、模板函数、显式具体化模板函数以及它们的重载版本。在调用函数时,显式具体化优先于常规模板,而非模板函数优先于显式具体化和常规模板。
类模板的显式具体化
除了函数模板,类模板也可以显式具体化,并且它们的语法是类似的。
1 | #include <iostream> |
运行结果:
1 | x=10, y=20 |
Point<char*, char*>
表明了要将类型参数 T1、T2
都具体化为 char*
类型,原来使用 T1、T2 的位置都应该使用
char*
替换。Point 类有两个类型参数
T1、T2,并且都已经被具体化了,所以整个类模板就不再有类型参数了,模板头应该写作
template<>
。
可以发现,当在类的外部定义成员函数时,普通类模板的成员函数前面要带上模板头,而具体化的类模板的成员函数不能带模板头。
部分显式具体化
在上面的显式具体化例子中,我们为所有的类型参数提供了实参,所以最后的模板头为空,也即
template<>
。另外 C++
还允许只为一部分类型参数提供实参,这称为部分显式具体化。
部分显式具体化只能用于类模板,不能由于函数模板。
仍然以 Point 为例,假设我们现在希望 "只要横坐标 x 是字符串类型" 就以
|
来分隔输出结果,而不管纵坐标 y
是什么类型,这种要求就可以使用部分显式具体化技术来满足。请看下面的代码:
1 | #include <iostream> |
输出:
1 | x=10, y=20 |
本例中,T1 对应横坐标 x 的类型,我们将 T1 具体化为
char*
。
模板头 template<typename T2>
中声明的是没有被具体化的类型参数;类名
Point<char*, T2>
列出了所有类型参数,包括未被具体化和已经被具体化的。
类名后面之所以要列出所有的类型参数,是为了让编译器确认
"到底是第几个类型参数被具体化了",如果写作
template<typename T2> class Point<char*>
,编译器就不知道
char*
代表的是第一个类型参数,还是第二个类型参数。
C++ 函数模板的实参类型推断
在使用类模板创建对象时,程序员需要显式的指明实参(也就是具体的类型)。例如对于下面的 Point 类:
1 | template<typename T1, typename T2> class Point; |
我们可以在栈上创建对象,也可以在堆上创建对象。
1 | Point<int, int> p1(10, 20); // 在栈上创建对象 |
因为已经显式地指明了 T1、T2 的具体类型,所以编译器就不用再自己推断了,直接拿来使用即可。
而对于函数模板,调用函数时可以不显式地指明实参(也就是具体的类型)。请看下面的例子:
1 | // 函数声明 |
虽然没有显式地指明 T 的具体类型,但是编译器会根据 n1 和 n2、f1 和 f2 的类型自动推断出 T 的类型。
这种通过函数实参来确定模板实参(也就是类型参数的具体类型)的过程称为模板实参推断。
在模板实参推断过程中,编译器使用函数调用中的实参类型来寻找类型参数的具体类型。
模板实参推断过程中的类型转换
对于普通函数(非模板函数),发生函数调用时会对实参的类型进行适当的转换,以适应形参的类型。这些转换包括:
算数转换:例如 int 转换为 float,char 转换为 int,double 转换为 int 等。
派生类向基类的转换:也就是向上转型。
const
转换:也即将非const
类型转换为 const 类型,例如将char *
转换为const char *
。数组或函数指针转换:如果函数形参不是引用类型,那么数组名会转换为数组指针,函数名也会转换为函数指针。
用户自定的类型转换。
例如有下面两个函数原型:
1 | void func1(int n, float f); |
它们具体的调用形式为:
1 | int nums[5]; |
而对于函数模板,类型转换则受到了更多的限制,仅能进行 const 转换和 数组或函数指针转换,其他的都不能应用于函数模板。
例如有下面几个函数模板:
1 | template<typename T> void func1(T a, T b); |
它们具体的调用形式为:
1 | int name[20]; |
对于 func1(12.5, 30)
,12.5 的类型为 double,30 的类型为
int,编译器不知道该将 T 实例化为 double 还是 int,也不会尝试将 int
转换为 double,或者将 double 转换为 int,所以调用失败。
请读者注意 name,它本来的类型是 int[20]
:
对于
func2(name)
和func4(name)
,name 的类型会从 int[20] 转换为 int,也即将数组转换为指针,所以 T 的类型分别为 int 和 int。对于
func5(name)
,name 的类型依然为 int[20],不会转换为 int*,所以 T 的类型为 int[20]。
可以发现,当函数形参是引用类型时,数组不会转换为指针。这个时候读者要注意下面这样的函数模板:
1 | template<typename T> void func(T &a, T &b); |
如果它的具体调用形式为:
1 | int str1[20]; |
由于 str1、str2 的类型分别为 int[20] 和 int[10],在函数调用过程中又不会转换为指针,所以编译器不知道应该将 T 实例化为 int[20] 还是 int[10],导致调用失败。
为函数模板显式地指明实参类型
函数模板的实参推断是指 "在函数调用过程中根据实参的类型来寻找类型参数的具体类型" 的过程,这在大部分情况下是奏效的,但是当类型参数的个数较多时,就会有个别的类型无法推断出来,这个时候就必须显式地指明实参。
下面是一个实参推断失败的例子:
1 | template<typename T1, typename T2> void func(T1 a) { |
func() 有两个类型参数,分别是 T1 和 T2,但是编译器只能从函数调用中推断出 T1 的类型来,不能推断出 T2 的类型来,所以这种调用是失败的,这个时候就必须显式地指明 T1、T2 的具体类型。
"为函数模板显式地指明实参" 和 "为类模板显式地指明实参"
的形式是类似的,就是在函数名后面添加尖括号
<>
,里面包含具体的类型。例如对于上面的
func(),我们这样来指明实参:
1 | func<int, int>(10); |
显式指明的模板实参会按照从左到右的顺序与对应的模板参数匹配:第一个实参与第一个模板参数匹配,第二个实参与第二个模板参数匹配,以此类推。只有尾部(最右)的类型参数的实参可以省略,而且前提是它们可以从传递给函数的实参中推断出来。
对于上面的 func(),虽然只有 T2 的类型不能自动推断出来,但是由于它位于类型参数列表的尾部,所以必须同时指明 T1 和 T2 的类型。对代码稍微作出修改:
1 | template<typename T1, typename T2> void func(T2 a) { |
由于 T2 的类型能够自动推断出来,并且它位于参数列表的尾部,所以可以省略。
显式地指明实参时可以应用正常的类型转换
上面我们提到,函数模板仅能进行 const 转换和 数组或函数指针转换 两种形式的类型转换,但是当我们显式地指明类型参数的实参时,就可以使用正常的类型转换(非模板函数可以使用的类型转换)了。
例如对于下面的函数模板:
1 | template<typename T> void func(T a, T b); |
它的具体调用形式如下:
1 | func(10, 23.5); //Error |
在第二种调用形式中,我们已经显式地指明了 T 的类型为 float,编译器不会再为 T 的类型到底是 int 还是 double 而纠结了,所以可以从容地使用正常的类型转换了。
C++ 函数模板的重载
当需要对不同的类型使用同一种算法(同一个函数体)时,为了避免定义多个功能重复的函数,可以使用模板。然而,并非所有的类型都使用同一种算法,有些特定的类型需要单独处理,为了满足这种需求,C++ 允许对函数模板进行重载,程序员可以像重载常规函数那样重载模板定义。
比如,交换两个变量的值,有两个方法,一个是使用指针,另一个是使用引用:
1 | // 方案1:使用指针 |
这两种方案都可以交换 int、float、char、bool 等基本类型变量的值,但是却不能交换两个数组。
对于方案1,调用阿函数时传入的是数组指针,或者说是指向第 0 个元素的指针,这样交换的仅仅是数组的第 0 个元素,而不是整个数组。数组和指针本来是不等价的,只是当函数的形参为指针时,传递的数组也会隐式地转换为指针。
对于方案2,假设传入的是一个长度为 3 的 int 类型数组(该数组的类型是
int[3]
),那么 T 的真实类型为
int[3]
,T temp
会被替换为
int [3]temp
,这显然是错误的。另外一方面,语句
a=b;
尝试对数组 a
赋值,而数组名是常量,它的值不允许被修改,所以也会产生错误。
交换两个数组唯一的办法就是逐个交换所有的数组元素,请看下面的代码:
1 | template<typename T> void Swap(T a[], T b[], int len) { |
在该函数模板中,最后一个参数的类型为具体类型(int),而不是泛型。并不是所有的模板参数都必须被泛型化。
下面是一个重载函数模板的完整示例:
1 | #include<iostream> |
输出:
1 | 99, 10 |
C++ 类模板入门
C++ 除了支持函数模板,还支持类模板(Class Template)。函数模板中定义的类型参数可以用在函数声明和定义中,类模板中定义的类型参数可以用在类声明和实现中。类模板的目的同样是将数据的类型参数化。
声明类模板的语法为:
1 | template<typename 类型参数1, typename 类型参数2, ...> class 类名 { |
类模板和函数模板都是以 template
开头(当然也可以使用
class,目前来讲它们没有任何区别),后跟类型参数;类型参数不能为空,多个类型参数用逗号隔开。
一旦声明了类模板,就可以将类型参数用于类的成员函数和成员变量了。换句话说,原来使用 int、float、char 等内置类型的地方,都可以用类型参数来代替。
假如我们现在要定义一个类来表示坐标,要求坐标的数据类型可以是整数、小数和字符串,例如:
x = 10、y = 10
x = 12.88、y = 129.65
x = "东经180度"、y = "北纬210度"
这个时候就可以使用类模板,请看下面的代码:
1 | template<typename T1, typename T2> // 这里不能有分号 |
x 坐标和 y 坐标的数据类型不确定,借助类模板可以将数据类型参数化,这样就不必定义多个类了。
注意:模板头和类头是一个整体,可以换行,但是中间不能有分号。
上面的代码仅仅是类的声明,我们还需要在类外定义成员函数。在类外定义成员函数时仍然需要带上模板头,格式为:
1 | template<typename 类型参数1, typename 类型参数2, ...> |
下面就对 Point 类的成员函数进行定义:
1 | template<typename T1, typename T2> // 模板头 |
请读者仔细观察代码,除了 template 关键字后面要指明类型参数,类名 Point 后面也要带上类型参数,只是不加 typename 关键字了。另外需要注意的是,在类外定义成员函数时,template 后面的类型参数要和类声明时一致。
使用类模板创建对象
上面的两段代码完成了类的定义,接下来就可以使用该类创建对象了。使用类模板创建对象时,需要指明具体的数据类型。请看下面的代码:
1 | Point<int, int> p1(10, 20); |
与函数模板不同的是,类模板在实例化时必须显式地指明数据类型,编译器不能根据给定的数据推演出数据类型。
除了对象变量,我们也可以使用对象指针的方式来实例化:
1 | Point<float, float> *p1 = new Point<float, float>(10.6, 109.3); |
需要注意的是,赋值号两边都要指明具体的数据类型,且要保持一致。下面的写法是错误的:
1 | // 赋值号两边的数据类型不一致 |
综合示例
将上面的类定义和类实例化的代码整合起来,构成一个完整的示例,如下所示:
1 | #include<iostream> |
运行结果:
1 | x=10, y=20 |
在定义类型参数时,我们使用了 class,而不是 typename,这样做的目的是让读者对两种写法都熟悉。
用类模板实现可变长数组:
1 | #include<iostream> |