编程思想——泛型编程

  泛型编程就是以独立于任何特定类型的方式编写代码。使用泛型程序时,我们需要提供具体程序实例操作的类型或只值。

  泛型编程和面向对象编程一样,都依赖于某种形式的多态性。

  面向对象编程中的多态性在运行时依赖存在继承关系的类。我们能够编写使用这些类的代码,忽略基类与派生类之间类型上的差异。只要使用基类的引用或指针,基类类型或派生类类型的对象就可以使用相同的代码。

  如果说面向对象是一种通过间接层来调用函数以换取一种抽象(创建一个接口类),那么泛型编程则是更直接的抽象,它不会因为间接层而损失效率。不同于面向对象的动态期多态,泛型编程是一种静态期多态,通过编译器生成最直接的代码。泛型编程可以将算法与特定类型、结构剥离,尽可能复用代码。标准库中的容器、迭代器和算法是很好的泛型编程的例子,几乎可以在任意类型上使用标准库的类和函数。

  C++中,模板是泛型编程的基础。模板是创建类或函数的蓝图或公式。

函数模板

  模板定义以关键字template开始,后接模板形参表。模板形参表是用尖括号括住的一个或多个模板形参的列表,形参之间以逗号分隔。模板形参表不能为空。

  模板形参表很像函数形参表,函数形参表定义了特定类型的局部变量但并不初始化那些变量,在运行时再提供实参来初始化形参。同样,模板形参表示可以在类或函数的定义中使用的类型或值。

1
2
3
4
5
6
7
//声明一个名为T的类型形参
//在callWithMax内部,可以使用名字T引用一个类型,具体表示哪个类型由编译器根据所用函数参数而确定
template<typename T>
void callWithMax(const T &a, const T &b)
{
f(a > b ? a : b);
}

  类型形参T跟在关键字class或者typename之后定义,这里class和typename没有区别。模板形参可以是表示类型的类型形参,也可以是表示常量表达式的非类型形参。模板形参选择的名字没有本质含义。可以给模板形参赋予的唯一含义是区别形参是类型形参还是非类型形参。如果是类型形参,我们就知道该形参表示的是未知类型;如果是非类型形参,我们就知道他是一个未知值。模板非类型形参是模板定义内部的常量值

  上面那个模板函数若再加上inline(不要放在template之前,要放在模板形参之后,返回类型之前),使其成为模板内联函数。这个模板内联函数可以用下列宏定义替换:

1
2
3
4
5
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))

int a = 5, b = 0;
CALL_WITH_MAX(++a, b); //这里a被累加两次
CALL+WITH_MAX(++a, b + 10); //这里a被累加一次

  由此可见,用模板内联函数可以避免宏定义中犯人的括号。而且还可以避免莫名其妙的情况,上面例子可见第一次运算的时候a的递增次数竟然依赖于它被拿来和谁比较。因此我们应该尽量少使用宏!!

  (宏定义和内联的区别:宏定义是在预处理阶段进行代码替换,内联函数是在编译阶段插入代码;其次宏没有类型检查,函数由类型检查)

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
/*
泛型编程的主要工作不是程序员,而是编译器
编译器会在编译期间根据代码自动生成方法
由于大量工作是编译器操作,可能通用的泛型函数并不能满足我们的需求
所以我们通常还需要“特化”处理
*/


//特化
//如果是字符串的话特殊处理
//其余的输入可以让输入类型不相同,返回值统一为int
template<>
char* max(char* a, char* b)
{
return (strcmp(a, b) > 0 ? (a) : (b));
}
template<class T1, class T2>
int max(T1 a, T2 b)
{
return static_cast<int>(a > b ? a : b);
}

#include <iostream>
using namespace std;
int main()
{
//调用函数
char* s1 = "hello";
char* s2 = "world";
cout << max(s1, s2) << endl;
cout << max(2, 4.5) << endl;
}

  泛型编程是把算法和具体的数据结构分开了,我们不需要考虑类型本身是什么,直接用一套逻辑把所有的类型都涵盖了,如果需要针对某些特殊类型做处理,我们就进行单独的“特化”。这里比较复杂的操作是编译器的推理过程,程序员所做的工作无非是把该定义好的类型通知编译器,让编译器帮助我们做处理。

类模板

1
2
3
4
5
6
7
8
9
10
11
12
13
template<class Type>
class Queue
{
public:
Queue();
Type &front();
const Type &front() const;
void push(const Type&);
void pop();
bool empty() const;
};

Queue<int> q1; //必须显式指定实参

  使用类模板时,必须为模板形参显式指定实参,编译器使用实参来实例化这个类的特定类型版本,重新编写Queue类。

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
/*
泛型编程的思想可以做一些算法优化,比如下面的等差数列求和
*/

// 1+2+3...+100 ==> n*(n+1)/2

//这里用泛型不是用泛型编程可以传递广泛类型的属性,这里直接定义了int
//这里是借用泛型编程的另一个特征:编译期推理,让程序自动生成代码
template<int n>
struct Sum
{
// 递推的思路,求前n个数的和可以表示为前(n-1)个数的和+n
// Sum(n) = Sum(n-1)+n
// 这里声明一个enum的成员,内部的枚举值是N,N的计算方法是递归求和
// 利用泛型编程的自动推理完成编译期计算
enum Value {
N = Sum<n-1>::N+n
};
};
//下面这个就是递推的基准点,n=1的特化
// 如果不设置,那么就会无穷递归
template<>
struct Sum<1>
{
enum Value {N = 1}; // n=1
};

int main()
{
//这里只需要输出结果就行,运算过程中在编译期就完成了
cout << Sum<100>::N << endl; //计算1+2+……+99+100
return 0;
}

  模板编程的难点很大程度上在于对编译器的理解,我们需要直到怎么帮助编译器提供需要生成代码的信息。