C++类模板

一个容器(队列、栈、链表)只能存储指定类型的对象(当然,基类指针容器可以用于存放派生类的对象),这就给编程带来很大的不便。我们希望我们能够编写一个其存储对象可变的容器,此时类模板就派上了用场。

类模板的简单使用

声明语法如下:

1
2
template <class Type> // in older compiler
template <typename Type> // in newer compiler

这里Type的名字是我们定义的,符合标识符的规范即可。

类模板的使用相对来说容易理解,这里不再赘述,只给出一段示例的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template  <class Type>
class Stack
{
private:
Type items[100];
int top;
public:
Stack();
bool isempty();
// ...
};

template <class Type> // in implementation, don't forget this
Stack<Type>::Stack()
{
// ...
}

// ...

Stack<int> kernels;
Stack<string> colonels;

需要注意的是,在声明时需要显式地提供类型,并且不同类型的Stack是不同的类,不可以相互赋值。

函数模板以及具体化

假如我们声明并定义了下面这个交换函数:

1
2
3
4
5
6
7
8
template <class T>
void Swap(T &a, T &b)
{
T temp;
temp = a;
a = b;
b = temp
}

于是在使用这个函数时,我们不需要为这个函数指定如何类型参数,直接将对应的参数传递给它即可。这样编译器会自动产生若干个Swap函数,分别对应我们每次调用的函数。在最终生成的可执行文件中不会包含函数模板,而是直接使用实际的参数替换我们的函数参数,于是会产生多份这个函数的定义。

局限,以及解决方法

函数模板会遇到一些问题,例如,在函数体中使用到*运算符,但是我们传入的模板参数string没有定义这个运算,那么就会出现问题。一种解决方案是通过重载运算符来实现,另一种解决方案是通过模板具体化来实现。具体化的意思是,提供一个具体的函数定义,当函数模板找到与函数调用匹配的函数是,就使用该定义,不再寻找模板。可以理解为通用的函数模板提供一个特例,这个特例不使用原来的函数模板。

第三代具体化

第三代具体化有以下特点:

  • 对于一个函数名,可以有非模板函数、模板函数和显式具体化函数模板以及它们的重载版本;
  • 显式具体化的原型和定义应以template<>打头,并通过名称来指出类型;
  • 具体化优先于常规模板,而非模板函数优先于具体化和常规模板。

具体化的代码如下:

1
2
template <> void Swap<Job>(Job &, Job &);
template <> void Swap(Job&, Job&); // simpler form

这样,当使用job对象作为参数调用Swap时,就会优先调用下面这个具体化的函数模板。

具体化和再定义一个普通的函数有什么区别?在调用上,可以显式地写上尖括号来使得编译器调用具体化的函数模板,否则编译器优先选择非模板函数。

实例化和具体化

辨析一下这两个概念,实例化是指编译器生成函数模板的一个实例(真正的代码,而非函数模板),具体化指的是程序员手动为函数模板指定一个某模板参数对应的函数,不让编译器使用原来“广泛”定义的函数,也可以理解为“特别化。

1.隐式实例化

隐式实例化很好理解,就是我们为函数传递参数时,编译器自动判断传入模板参数的类型并实例化一个函数,这个过程是隐式的,我们感知不到,所以称为隐式实例化。

2.显式实例化

显示实例化是指我们手动地命令编译器实例化指定类型的函数定义,具体语法为:

1
template void Swap<int>(int,int);

显式实例化可以更好地帮助编译器完成参数匹配:

1
2
3
4
5
6
template <class T>
T add(T a, T b){ return a+b}
//...
int m = 6;
double x = 10.2;
Add<double>(m,x);

这里虽然没有匹配的函数,但是我们显式实例化了一个函数,在调用时,就可以将int类型转化为double类型

3.显式具体化

显示具体化的概念上面已经提到过,就是形成一个函数模板的特殊版本,其语法上与显式实例化的区别就是显式具体化的声明多一对尖括号:

1
template <> void Swap<int>(int &,int &);

函数调用的匹配

编译器如何为一个函数调用匹配相应的函数,这是一个非常复杂的过程,但是明确的是编译器是有一套标准的规则的,并且当存在两个可以调用的函数时,编译器会报二义性错误。在使用的原则上说,不要定义两个非常相似的函数,以防引起二义性错误。

decltype关键字

decltype关键字在无法预知的值类型的情况下,可以声明一个变量,如下列代码所示:

1
2
3
4
5
template <class T1, class T2>
void ft(T1 x,T2 y)
{
decltype(x+y) xpy = x+y;
}

decltype(expression)的含义为,表达式expression所代表的值的类型,这个表达式如果是一个函数调用,则类型会与函数的返回类型相同,注意:编译器并不会实际调用函数,而是通过查看其返回值来获得返回类型

在频繁用到这个类型时,可以将typedef关键字与decltype关键字一起使用,这样可以减少一点代码量。

decltype与函数返回值

如果无法预知函数的返回值,则可以使用后置返回类型来完成这个操作,后置返回类型将返回值的声明置于已声明的参数后,使得返回类型较为可知。

1
2
3
4
5
6
template <class T1,class T2>
auto gt(T1 x, T2 x) -> decltype(x+y)
{
// ...
return x+y;
}

类模板的实例化

类模板的实例化和函数模板的实例化非常相似,在概念上也很相近。隐式实例化指的是在声明一个或多个对象时编译器自动根据模板所提供的处方来生成具体的类定义。显示实例化使用一个声明语句来让编译器显示地生成一个类:

1
template class ArrayTP<string, 100>;

显式具体化

显式具体化也和函数模板的定义相似,就是为类模板提供一个特殊的模板参数,使得这个类模板在实例化的行为与普通模板不同。具体语法为:

1
template <> class Classname<int>{ /*...*/ };

第一个尖括号内是暂时没有被指定的模板参数,后一个尖括号内是被指定的模板参数。如果所有的模板参数都被指定,那么就是显式具体化,如果有一个以上的模板参数没有被具体化,那么就是部分具体化。编译器在选择类模板时,会选择具体化程度最高的模板。

更高级的类型参数

类型参数中不止可以使用类,还可以使用像函数一样的参数来规定容器的大小,具体见代码:

1
2
3
4
5
6
7
8
9
template <class T,int n>
class ArrayTP
{
private:
T ar[n];
// ...
}

ArrayTP<double, 10> eggweights;

这样的参数称为非类型表达式参数。

这样的参数有一定的限制,例如,这里类中不能修改模板参数的值,所以n的值不能被修改,另外需要注意的是,n不同,所生成的ArrayTP对象也不应该被看成是同一个类型的对象。

总的来说,不应该把这里的n看成一个参数,而是一个即将用于替换下文代码的值。

默认模板类型参数

望文生义即可知道,这个特性应该和函数的默认参数很像,如果不提供对应位置的参数就会使用声明时的类型参数来替代。这里也不再赘述。

将模板用作参数

模板除了可以包含类型参数和非类型参数,还可以包含本身就是模板的参数:

1
2
template <template <typename T> class Thing>
class Crab

这样的声明意味着,假如有以下的声明:

1
Crab<King> legs;

那么King的值必须与模板参数的说明匹配,例如:

1
2
template<typename T>
class King{ /*...*/};

这个使用方法可能比较难理解,我个人的理解是:目前已经有一个容器类(如栈、队列),然后现在我想声明一个新的类,其中包含了若干个容器,那么就可以用这个技巧,让我们的新类中包含这些容器。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
template <template <typename T> class Thing>
class MyClass
{
private:
Thing<int> s1;
Thing<double> s2;
};

int main()
{
MyClass<vector> t1;
}

这样就获得了一个新的类,这个类中包含两个成员变量,一个是vector<int>,一个是vector<double>。更高端的用法是混合使用模板和类型参数,这样新类的两个容器所存放的数据类型就可以由我们自己定义。

网上看到一个很好地类比:将模板类作为模板参数就好像将一个函数指针作为另一个函数的参数,都是为了方便另一个类/函数来使用。

模板类与友元

如果在模板类中像之前那样使用友元,那么情况会是这样的:这个友元会成为所有实例化模板的友元。在某些情况下(例如我们希望读取一个类被实例化为几个对象时),就应该让友元所能访问的模板隔离开来,就需要将友元也实例化。

非模板友元

1
friend void report(HasFriend<T> &);

通过这样的声明,可以使得各个实例化模板的友元隔离开。需要注意的是,这样的友元并不是函数模板,它只是将一个模板作为参数,所以只用前还需要提供显示具体化:

1
2
3
4
void report(HasFriend<int> &hf)
{
// definitions
}

模板类的约束模板友元函数

约束模板友元函数首先声明模板函数,然后再在类的声明中将模板函数本身成为模板(或者说,使得类的每一个具体化都获得与友元匹配的具体化):

1
2
3
4
5
6
template <typename T> void report(T &);
template <typename TT>
class HasFriendT
{
friend void report<>(HasFriend<TT> &);
}

模板类的非约束模板友元函数

还可以在类内部声明模板,使得函数的所有具体化都是每个类具体化的友元,即这个友元可以访问哪些数据是由调用时的参数决定的:

1
2
3
4
5
template <typename T>
class ManyFriend
{
template <typename C, typename D> friend void show(C &,D &);
}

在调用show函数时,其就会根据参数类型推断出C和D的类,并可以访问其私有成员。


C++类模板
http://zhf-xdu.top/2023/11/13/C-模板类/
作者
周洪锋
发布于
2023年11月13日
许可协议