C++ note (13)
Eiaton

模板

泛型编程

泛型编程(generic programming)

  • 独立于任何特定数据类型的编程,这使得不同类型的数据(对象)可以被相同的代码操作

在 C++ 中,使用模板(template)来进行泛型编程,包括

  • 函数模板(Function template)
  • 类模板(Class template)

当从通用代码创建实例代码时,具体数据类型才被确定

泛型编程是一种编译时多态性(静态多态)。其中,数据类型本身是参数化的,因而程序具有多态性特征

实例化

实例化(Instantiation):由编译器将通用模板代码转换为不同的实例代码的过程称为实例化

函数模板(Function template)

概念

用相同的处理过程,处理不同类型的数据

  • 减少代码
  • 甚至能处理编程时未知的数据类型

    已知函数,类,未知处理的数据类型,模板

    已知基类的虚函数,未知派生类的具体实现,覆盖(多态)

    已知函数功能,未知具体参数类型与组合,重载

举例

1
2
3
4
5
6
void swap(int& v1, int& v2) {
int tmp;
tmp = v1;
v1 = v2;
v2 = tmp;
}
1
2
3
4
5
6
void swap(double& v1, double& v2) {
double tmp;
tmp = v1;
v1 = v2;
v2 = tmp;
}
1
2
3
4
5
6
void swap(string& v1, string& v2) {
string tmp;
tmp = v1;
v1 = v2;
v2 = tmp;
}

此三个函数除了所处理对象的类型不同之外,代码几乎完全相同。即,三个函数功能类似。以下函数模板可以涵盖以上三函数的作用:

1
2
3
4
5
6
7
template<typename T>
void swap(T& v1, T& v2) {
T tmp;
tmp = v1;
v1 = v2;
v2 = tmp;
}

注意:程序有些 bug。当 T 存在默认构造函数时,编译错误。

正确的写法: T tmp = v1; 即使用 copy 构造,来避免使用默认初始化

一般形式

1
2
3
template<模板形参表> 返回值类型 函数名 (形参列表) {
函数体
}
  1. 模板形参表不能为空
  2. 形参列表必须包含模板形参表中出现的所有形参

模板形参表的形式:

  • typename 模板形参1, typename 模板形参2, …

实例化(instantiation)

函数模板的使用形式与普通函数调用相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main() {
std::string s1("rabbit"), s2("bear");
int iv1 = 3, iv2 = 5;
double dv1 = 2.8, dv2 = 8.5;

// 调用函数模板的实例 swap(string&, string&)
swap(s1, s2);

// 调用函数模板的实例 swap(int&, int&)
swap(iv1, iv2);

// 调用swap的实例 swap(double&, double&)
swap(dv1, dv2);
}

调用函数模板的过程:

  1. 模板实参推断(template argument deduction):编译器根据函数调用中所给出的实参的类型,确定相应的模板实参
  2. 函数模板的实例化(instantiation):模板实参确定之后,编译器就使用模板实参代替相应的模板形参,产生并编译函数模板的一个特定版本(称为函数模板的一个实例(instance))(注意:此过程中不进行常规隐式类型转换

显示(全)模板特化

  • 在模板当中有些特殊的类型,当想要针对特殊的类型进行一些特殊的操作,这时候就可以用模板的特化
  • 在正常的模板下面接着编写代码,写一个空的template<>然后写个具体的函数代码来补充
  • 如实例所示,当传入的实参类型是int类型,就执行模板的特化部分,而非int类型执行正常的模板推断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <typename T> void swap(T& v1, T& v2) {
T tmp;
tmp = v1;
v1 = v2;
v2 = tmp;
}

template <> void swap(int& v1, int& v2) {
int tmp;
tmp = v1;
v1 = v2;
v2 = tmp;
v1 += 10;
v2 += 10;
}

模板重载

对函数模板进行重载:

  • 定义名字相同而函数形参表不同的函数模板
  • 或者定义与函数模板同名的非模板函数(正常函数),在其函数体中完成不同的行为

编译器是如何确定调用的是这么多同名函数中的哪一个呢?

如何确定调用哪个函数

函数调用的静态绑定规则(重载协议):

  1. 如果某一同名非模板函数(指正常的函数)的形参类型正好与函数调用的实参类型匹配(完全一致),则调用该函数。否则,进入第2步
  2. 如果能从同名的函数模板实例化一个函数实例,而该函数实例的形参类型正好与函数调用的实参类型匹配(完全一致),则调用该函数模板的实例函数。否则,进入第3步
    • 在步骤2中:首先匹配函数模板的特化,在匹配非指定特殊的函数模板
  3. 对函数调用的实参进行隐式类型转换后与非模板函数再次进行匹配,若能找到匹配的函数则调用该函数。否则,进入第4步
  4. 提示编译错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 函数模板demoPrint
template <typename T>
void demoPrint(const T v1, const T v2){
cout << "the first version of demoPrint()" << endl;
cout << "the arguments: " << v1 << " " << v2 << endl;
}

// 函数模板demoPrint的指定特殊
template <>
void demoPrint(const char v1, const char v2){
cout << "the specify special of demoPrint()" << endl;
cout << "the arguments: " << v1 << " " << v2 << endl;
}

// 函数模板demoPrint重载的函数模板
template <typename T>
void demoPrint(const T v){
cout << "the second version of demoPrint()" << endl;
cout << "the argument: " << v << endl;
}

// 非函数模板demoPrint
void demoPrint(const double v1, const double v2){
cout << "the nonfunctional template version of demoPrint()" << endl;
cout << "the arguments: " << v1 << " " << v2 << endl;
}

/* 函数调用 */
string s1("rabbit"), s2("bear");
char c1('k'), c2('b');
int iv1 = 3, iv2 = 5;
double dv1 = 2.8, dv2 = 8.5;

// 调用第一个函数模板
demoPrint(iv1, iv2);

// 调用第一个函数模板的指定特殊
demoPrint(c1, c2);

// 调用第二个函数模板
demoPrint(iv1);

// 调用非函数模板
demoPrint(dv1, dv2);

// 隐式转换后调用非函数模板
demoPrint(iv1, dv2);
1
2
3
4
5
6
7
8
9
10
11
/* 结果 */
the first version of demoPrint()
the arguments: 3 5
the specify special of demoPrint()
the arguments: k b
the second version of demoPrint()
the argument: 3
the nonfunctional template version of demoPrint()
the arguments: 2.8 8.5
the nonfunctional template version of demoPrint()
the arguments: 3 8.5

结论:函数模板是不进行隐式转换的,只有非函数模板才进行隐式转换

类模板

使用情景定义可以存放任意类型对象的通用容器类

  • 定义一个栈(stack)类,既可用于存放int型对象,又可用于存放float、double、string…甚至任意未知类型的元素
  • 定义一个队列(queue)类,即可用于存放int型对象,又可用于存放float、double、string…甚至任意未知类型的元素

实现方式:为类声明一种模板,使得类中的某些数据成员、某些成员函数的参数、某些成员函数的返回值,能取任意类型(包括基本类型和用户自定义类型)

定义方式

类模板的一般形式:

1
2
3
4
5
template <模板形参表>
class 类模板名 {
类成员声明
...
}

在类模板外定义成员函数的一般形式:

1
2
3
4
5
template <模板形参表>
返回值类型 类模板名<模板形参名列表>::函数名(函数形参表) {
函数实现
...
}

其中模板形参表的形式为:template <typename 类型参数1, typename 类型参数2, …>

(注:模板形参每项是非类型形参、类型形参、模板形参之一。)

示例:链表实现的栈类模板

类模板的实例化

  • 类模板是一个通用类模型,而不是具体类,不能用于创建对象,只有经过实例化后才得到具体类,才能用于创建对象
  • 实例化的一般形式:
    • 类模板名 < 模板实参表 >
  • 模板实参是一个实际类型,如int,double等
  • 一个类模板可以实例化为多个不同的具体类
    • Stack stack_int
    • Stack stack_double
    • Stack stack_string

类模板的文件组织形式

  • 一般而言,调用函数时,编译器只需要看到函数的声明即可。所以可以把函数的声明放在 .h 文件中,实现在 .cpp 的实现文件中,使用函数的地方#include 函数的 .h 文件即可

  • 对于模板则不同,要进行实例化,编译器必须能够访问模板定义的源代码

  • 为了在模板中也实现一般的声明定义分离,C++提供了两种模板的编译模型:

  • 包含编译模式(inclusion compilation model)

    • 要求:在函数模板或类模板成员函数的调用点,相应函数的定义对编译器必须是可见的
    • 实现方式:在头文件中用#include包含实现文件(也可将模板的实现代码直接放在头文件中)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      //genericStack.h

      #ifndef GSTACK_H
      #define GSTACK_H

      类模板的定义和实现代码

      #endif

      1
      2
      3
      4
      5
      6
      7
      8
      //client.cpp客户代码
      #include “genericStack.h”
      int main()
      {
      Stack<int> stack;
      for (int i = 1; i < 9; i++) stack.push(i);
      }

  • 分离编译模式(separate compilation model)

    • 要求:声明和定义分离,程序员在实现文件中使用保留字export告诉编译器,需要记住哪些模板定义。
    • 不是所有编译器都支持该模式

非类型模板形参

  • 两类模板形参:类型模板形参和非类型模板形参
    非类型模板形参:
    • 相当于模板内部的常量
    • 形式上类似于普通的函数形参
    • 对模板进行实例化时,非类型形参由相应模板实参的值代替
    • 对应的模板实参必须是编译时常量表达式

示例1:以数组实现的栈类模板

示例2:打印函数

不使用非模板形参实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template <typename T>
void printValues(T* arr, int N) {
for (int i =0; i != N; ++i)
cout<< arr[i] << endl;
}


int main()
{
int intArr[6] = {1, 2, 3, 4, 5, 6};
double dblArr[4] = {1.2, 2.3, 3.4, 4.5};

// 生成函数实例printValues(int*, 6)
printValues(intArr, 6);

// 生成函数实例printValues(double*, 4)
printValues(dblArr, 4);

return 0;
}