高级程序设计

21 - 模板

2020-04-16 14:00 CST
2020-04-16 12:15 CST
CC BY-NC 4.0

在程序设计中,经常需要用到一些功能完全相同的程序实体,但它们涉及的数据类型不同。

  • 不同类型数据的排序算法
  • 不同元素类型的数据结构

如果能只用一个函数/一个类来描述这些函数/结构,可以简化程序设计工作。

范型(类属)

一个程序实体能对多种类型的数据进行操作或描述的特性称为类属或范型(generics)。

具有类属特性的程序实体通常有:

  • 类属函数:一个能对不同类型的数据完成相同操作的函数
  • 类属类:一个成员类型可变,但功能不变的类

基于具有类属性的程序实体进行程序设计的技术称为范型程序设计(类属程序设计,generic programming)。

类属函数

类属函数是指一个函数能对不同类型的参数完成相同的操作。

C++提供了两种实现类属函数的机制:

  • 采用通用指针类型的参数(C语言)
    • void *指针+函数指针
    • 问题:需要大量的指针操作,实现麻烦;编译器无法进行类型检查,容易出错。
  • 函数模板

函数模板

函数模板是指带有类型参数的函数定义,格式为

template <class T1, class T2, ...>
<返回值类型> <函数名> (<参数表>) {
  // ......
}

其中T1T2等是函数模板的参数,函数的返回值类型、参数表中的参数以及函数体中的局部变量的类型可以是T1T2等。

函数模板的实例化

实际上,函数模板定义了一系列重载的函数。

要使用函数模板所定义的函数,首先必须要对函数模板进行实例化:给模板参数提供一个具体的类型,从而生成具体的函数。

函数模板的实例化通常是隐式的:

  • 由编译程序根据函数调用的实参类型自动地把函数模板实例化为具体的函数。
  • 这种确定函数模板实例的过程叫做模板实参推导(template argument deduction)。

有时编译程序无法根据调用时的实参类型来确定所调用的模板实例函数,此时需要在程序中显式地实例化模板函数。例如:

int x;
double y;

max(x, y); // error
max((double)x, y); // OK
max(x, (int)y);    // OK
max<int>(x, y);    // OK
max<double>(x, y); // OK

带非类型参数的函数模板

除了类型参数外,函数模板还可以带有非类型参数。如:

template <class T, int size>
void foo(T a) {
  T temp[size];
  // ......
}

这样的函数模板在使用时需要显式实例化。

类模板

如果一个类的成员类型可变,但功能不变,则该类称为类属类。

在C++中,类属类用类模板实现:

template <class T1, class T2>
class <类名> {
  <类成员说明>
};

除了类型参数外,类模板也可以包括非类型参数。

类模板的实例化

类模板定义了若干个类,在使用这些类之前,编译程序会对类模板进行实例化。

类模板的实例化需要在程序中显式地指出。

不同类模板之间不共享类模板中的静态成员。

模板的复用

用模板实现的类属也属于一种多态,称为“参数化多态”。

使用一个模板之前首先要对其实例化,而实例化是在编译时刻进行的,必须要见到相应的源代码,因此模板属于源代码复用。

一个模板可以有很多的实例,是否实例化模板的某个实例需要根据使用情况来决定。通常情况下把模板的定义和实现都放在头文件中,使用者通过包含这个头文件,把模板的源代码全部包含进来,防止出现未定义错误。

重复实例的处理

模板的复用会导致多模块的编译结果中存在相同的实例:

  • 相同的函数模板实例
  • 相同的类模板成员函数实例

相同代码的存在会导致目标代码庞大。

解决方案:

  • 开发环境:编译第二个模块的时候不生成这个实例(代价较大)
  • 链接程序:舍弃一个相同的实例

类模板的友元函数

template <class T>
class A {
  friend void f1(A<T> &a);
  template <class T> friend void f2(A<T> &a);
  friend void f3<T>(A<T> &a);
};
  • f1不是模板函数,只能存在一个
    • 如果定义f1void f1(A<int> &a)
    • f1(A<int> &a)A<int>的友元
    • f1(A<double> &a)不存在
  • f2的实例和A的实例是多对多友元
    • f2<int>A<int>是友元
    • f2<int>A<double>也是友元
  • f3的实例和A的实例是一对一友元
    • f3<int>A<int>是友元
    • f3<int>A<double>不是友元