C++ Section3 泛型编程

模板通式

函数模板通式

1
2
3
4
template <class 形参名,class 形参名,...>
返回类型 函数名(参数列表) {
函数体
}

类模板通式

1
2
3
template <class 形参名,class 形参名, ...>
class 类名
{ ... };

模板的非类型形参

  1. 非类型模板形参:模板的非类型形参也就是内置类型形参,如template class B{};其中int a就是非类型的模板形参。

  2. 非类型形参在模板定义的内部是常量值,也就是说非类型形参在模板的内部是常量。

  3. 非类型模板的形参只能是整型指针引用,像double,String, String **这样的类型是不允许的。但是double &,double *,对象的引用或指针是正确的。

  4. 调用非类型模板形参的实参必须是一个常量表达式,即他必须能在编译时计算出结果。

  5. 注意:任何局部对象,局部变量,局部对象的地址,局部变量的地址都不是一个常量表达式,都不能用作非类型模板形参的实参。全局指针类型,全局变量,全局对象也不是一个常量表达式,不能用作非类型模板形参的实参。

  6. 全局变量的地址或引用,全局对象的地址或引用const类型变量是常量表达式,可以用作非类型模板形参的实参。

  7. sizeof表达式的结果是一个常量表达式,也能用作非类型模板形参的实参。

  8. 当模板的形参是整型时调用该模板时的实参必须是整型的,且在编译期间是常量,比如template class A{};如果有int b,这时A m;将出错,因为b不是常量,如果const int b,这时A m;就是正确的,因为这时b是常量。

  9. 非类型形参一般不应用于函数模板中,比如有函数模板template void h(T b){},若使用h(2)调用会出现无法为非类型形参a推演出参数的错误,对这种模板函数可以用显示模板实参来解决,如用h(2)这样就把非类型形参a设置为整数3。显示模板实参在后面介绍。

  10. 非类型模板形参的形参和实参间所允许的转换

    • 允许从数组到指针,从函数到指针的转换。如:template class A{}; int b[1]; A\ m;即数组到指针的转换
    • const修饰符的转换。如:template class A{}; int b; A\<&b> m; 即从int *到const int *的转换。
    • 提升转换。如:template class A{}; const short b=2; A\ m; 即从short到int的提升转换
    • 整值转换。如:template class A{}; A<3> m; 即从int 到unsigned int的转换。
    • 常规转换。

非类型模板的应用:Stack类

stack.h

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
48
49
50
51
52
#ifndef STACK_H
#define STACK_H
template <class T,int MAXSIZE>
class Stack {
private:
T elems[MAXSIZE];
int numElems;
public:
Stack();
void push(T const&);
void pop();
T top() const;
bool empty() const{
return numElems == 0;
}
bool full() const{
return numElems == MAXSIZE;
}
};
template <class T,int MAXSIZE>
Stack<T,MAXSIZE>::Stack():numElems(0){
}
template <class T,int MAXSIZE>
void Stack<T, MAXSIZE>::push(T const& elem){
if(numElems == MAXSIZE){
throw std::out_of_range("Stack<>::push(): stack is full");
}
elems[numElems] = elem;
++numElems;
}
template<class T,int MAXSIZE>
void Stack<T,MAXSIZE>::pop(){
if (numElems <= 0) {
throw std::out_of_range("Stack<>::pop(): empty stack");
}
--numElems;
}
template <class T,int MAXSIZE>
T Stack<T,MAXSIZE>::top() const{
if (numElems <= 0) {
throw std::out_of_range("Stack<>::top(): empty stack");
}
return elems[numElems-1];
}
#endif

stack.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <string>
#include "stack.h"
int main(){
Stack<int,20> stack_int; // 可以存储20个int元素的栈
Stack<std::string,40> stack_str; // 可存储40个string元素的栈
// 使用可存储20个int元素的栈
stack_int.push(7);
std::cout << stack_int.top() << std::endl; //7
stack_int.pop();
// 使用可存储40个string的栈
stack_str.push("hello");
std::cout << stack_str.top() << std::endl; //hello
stack_str.pop();
stack_str.pop(); //Exception: Stack<>::pop<>: empty stack
return 0;
}

模板的全特化和偏特化

所谓特化,就是将泛型的东西搞得具体化一些,从字面上来解释,就是为已有的模板参数进行一些使其特殊化的指定,使得以前不受任何约束的模板参数,或受到特定的修饰(例如const或者摇身一变成为了指针之类的东东,甚至是经过别的模板类包装之后的模板类型)或完全被指定了下来。

模板有两种特化,全特化偏特化(局部特化)

  • 模板函数只能全特化,没有偏特化(以后可能有)。
  • 模板类是可以全特化和偏特化的。

全特化,就是模板中模板参数全被指定为确定的类型。全特化也就是定义了一个全新的类型,全特化的类中的函数可以与模板类不一样。

偏特化,就是模板中的模板参数没有被全部确定,需要编译器在编译时进行确定。 在类型上加上const、&、*( cosnt int、int&、int*、等等)并没有产生新的类型。只是类型被修饰了。模板在编译时,可以得到这些修饰信息。

模板为什么要特化,因为编译器认为,对于特定的类型,如果你能对某一功能更好的实现,那么就该听你的。
模板分为类模板函数模板,特化分为全特化偏特化。全特化就是限定死模板实现的具体类型,偏特化就是如果这个模板有多个类型,那么只限定其中的一部分。

先看类模板:

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
template<typename T1, typename T2>
class Test
{
public:
Test(T1 i,T2 j):a(i),b(j){cout<<"模板类"<<endl;}
private:
T1 a;
T2 b;
};
template<>
class Test<int , char>
{
public:
Test(int i, char j):a(i),b(j){cout<<"全特化"<<endl;}
private:
int a;
char b;
};
template <typename T2>
class Test<char, T2>
{
public:
Test(char i, T2 j):a(i),b(j){cout<<"偏特化"<<endl;}
private:
char a;
T2 b;
};

那么下面3句依次调用类模板、全特化与偏特化:

1
2
3
Test<double , double> t1(0.1,0.2);
Test<int , char> t2(1,'A');
Test<char, bool> t3('A',true);

而对于函数模板,却只有全特化,不能偏特化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//模板函数
template<typename T1, typename T2>
void fun(T1 a , T2 b)
{
cout<<"模板函数"<<endl;
}
//全特化
template<>
void fun<int ,char >(int a, char b)
{
cout<<"全特化"<<endl;
}
//函数不存在偏特化:下面的代码是错误的
/*
template<typename T2>
void fun<char,T2>(char a, T2 b)
{
cout<<"偏特化"<<endl;
}
*/

至于为什么函数不能偏特化,似乎不是因为语言实现不了,而是因为偏特化的功能可以通过函数的重载完成。

函数模版的全特化不参与函数重载, 并且优先级低于函数基础模版参与匹配,也就是说,匹配的顺序是:

  1. 非模板函数
  2. 某个没有进行全特化的template function
  3. 如果这个没有进行全特化的template function有全特化版本,并且类型也比较匹配,则选择这个全特化版本