C++之模板与泛型编程

在C++中,要定义一个通用类型的变量或者函数时,就需要泛型编程。模板是C++中实现泛型编程的一种手段。


函数模板

模板函数的格式:

1
2
3
4
template <typename p1, typename p2, typename p3, ...>
返回值类型 函数名(参数列表) {
函数体;
}

函数模板代表了一个函数家族,该函数与类型无关,在使用的时候被参数化,根据实参类型产生函数的特定类型版本。
在代码中包含函数模板本身并不会生成函数定义,它只是用于生成函数定义的方案,编译器使用模板为特定类型生成函数定义时,得到的是模板实例。编译器用模板产生指定的类或者函数的特定类型版本,产生模板特定类型的过程称为函数模板实例化。编译器会用推断出的模板参数为我们实例化一个特定版本的函数。当编译器实例化一个模板时,它使用实际的模板实参代替对用的模板参数来创建一个模板的新实例。


隐式实例化

隐式实例化是编译器根据我们调用的参数来推断实例化模板所需的类型,这叫隐式实例化。

显示实例化

显示实例化需要我们指定创建特定类型的实例,其用法是:用<>指定类型,并在声明前加上template。
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;
template <typename T>
T Add(T a, T b) {
return a + b;
}
template int Add(int a, int b); // 显示实例化
int main() {
int a = 3, b = 4;
double c = 2.3, d = 12.4;
int e = Add(a, b);
double f = Add(c, d); // 隐式实例化
return 0;
}


模板类型参数

在上述例子中,Add函数有一个模板类型参数,可以将类型参数看作是类型说明符。


非类型模板参数

在模板中,除了可以定义模板类型参数,还可以定义非类型模板参数,它表示一个值而非一个类型。
我们可以通过一个指定的类型名而非关键字class/typename来指定非类型参数。当一个模板被实例化时,非类型参数被一个编译器推断出的值所代替。这些值必须是常量表达式,从而允许编译器在编译实例化模板。
模板形参说明:

1、模板形参表使用<>括起来
2、和函数参数表一样,跟多个参数时必须用逗号隔开,类型可以相同也可以不相同
3、模板形参表不能为空
4、模板形参可以是类型形参,也可以是非类型新参,类型形参跟在class和typename后
5、模板类型形参可作为类型说明符用在模板中的任何地方,与内置类型或自定义类型 使用方法完全相同,可用于指定函数形参类型、返回值、局部变量和强制类型转换
6、模板形参表中,class和typename具有相同的含义,可以互换,使用typename更加直观。


模板函数重载

和常规的重载一样,被重载的模板的函数的特征必须不同。
即:参数不同或者返回值不同。

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
#include <iostream>
using namespace std;
int Max (const int &left, const int &right) {
// 1
cout << 1 << endl;
return left>right ? left : right;
}
template <typename T>
T Max (const T &left, const T &right) {
// 2
cout << 2 << endl;
return left>right ? left : right;
}
template <typename T>
T Max (const T &a, const T &b, const T &c) {
// 3
cout << 3 << endl;
return a>b ? (a>c ? a : c) : (b>c ? b : c);
}
int main() {
Max(10, 20, 30); // 3
Max<>(10, 20); // 2
Max(10, 20); // 1
Max(10, 20.12); // 1
Max<int>(10.0, 20.0); // 2
Max(10.0, 20.0); // 2
return 0;
}

1、一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
2、对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调动非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数,那么将选择模板。
3、显式指定一个空的模板实参列表,该语法告诉编译器只有模板才能来匹配这个调用,而且所有的模板参数都应该根据实参演绎出来。
4、模板函数不允许自动类型转换,但普通函数可以进行自动类型转换。这是Max(10, 20.12)调用原来的函数而不是模板函数的原因。


模板函数的特化

接着上述的例子,如果我们比较两个指针指向内容的大小,而不是比较两个指针本身的大小,这时候需要对模板函数特化。

1
2
3
template<>
int compare<const char*> (const char *const p1, const char *const p2) {
return strcmp(p1, pt);

注意:在模板特化版本的调用中,实参类型必须与特化版本函数的形参类型完全匹配。


模板编译

当编译器遇到一个模板定义时,它并不生成代码。只有当我们实例化出模板的一个特定版本时,编译器才会生成代码。当我们使用模板时,编译器才生成代码。
我们使用一个类类型的对象时,类定义必须是可用的,但成员函数的定义不必已经出现。因为我们可以把类定义和函数声明放在头文件中,而普通函数和类的成员函数的定义可以放在源文件中。
但是在模板中,为了实例化,编译器需要掌握模板的定义,而不是声明,所以模板的头文件通常包括声明也包括定义。
通常,编译器会在三个阶段报告模板错误:
1、第一个阶段是编译模板本身时:
在这个阶段,编译器通常不会发现很多错误。编译器可以检查语法错误,例如忘记分号或者变量名写错等等。
2、第二个阶段是编译器遇到模板使用时:
在此阶段,编译器仍然没有很多检查的。对于函数模板调用,编译器通常会检查实参数目是否正确。他还能检查参数类型是否匹配,对于类模板,编译器可以检查用户是否提供了正确数目的模板实参,但也仅限于此了。
3、第三个阶段是模板实例化时,只有这个阶段才能发现类型相关的错误:
依赖于编译器如何管理实例化,这类错误可能在链接时才报告。


类模板

类模板的格式:

1
2
3
template <typename p1, typename p2, typename p3, ...>
class 类名 {
};

模板类实例化

类模板是用来生成类的蓝图的。与函数模板的不同之处是:编译器不能为类模板推断模板参数类型。为了使用类模板,我们必须在模板名后的尖括号中提供额外信息——用来代替模板参数的模板实参列表。
一个类的模板的每个实例都会形成一个独立的类。


类模板的成员函数

类模板的成员函数本身是一个普通函数,但在类模板的每一个实例中都有自己版本的成员函数,因此类模板的成员函数具有和模板相同的模板参数。
因此,定义在类模板之外的成员函数就必须以关键字template开始,后接模板参数列表。


类模板特化

全特化

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
#include <iostream>
using namespace std;
template <typename T1, typename T2>
class Data {
public:
Data();
private:
T1 m_d1;
T2 m_d2;
};
template <typename T1, typename T2>
Data<T1, T2>::Data() {
cout << "Data<T1, T2>" << endl;
}
// 全特化
template <>
class Data<int, int> {
public:
Data();
private:
int m_d1;
int m_d2;
};
Data<int, int>::Data() {
cout <<"Data<int, int>" << endl;
}
int main() {
Data<int, int> d1;
Data<int, double> d2;
return 0;
}

偏特化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 偏特化
template <typename T1>
class Data<T1, int> {
public:
Data();
private:
T1 m_d1;
int m_d2;
};
template <typename T1>
Data<T1, int>::Data() {
cout <<"Data<T1, int>" << endl;
}

参考资料:
C++泛型编程1——函数模板实例化,模板参数,重载及特化
C++泛型编程2——类模板,容器适配器,仿函数