Cpp Learning Notes-Template

C++泛型编程—-模板

泛型程序设计(generic programming)是一种算法在实现时不指定具体要操作的数据的类型的程序设计方法。所谓“泛型”,指的是算法只要实现一遍,就能适用于多种数据类型。泛型程序设计方法的优势在于能够减少重复代码的编写。

为了实现泛型编程, C++中支持模板的概念.模板就是将数据类型也作为一个参数传递到具体的算法实现中(类型参数化),这样就实现了算法和数据类型的分离,即针对不同的数据类型,同一个模板都能正确处理.

函数模板

所谓函数模板,实际上是建立一个通用函数,它所用到的数据的类型(包括返回值类型、形参类型、局部变量类型)可以不具体指定,而是用一个虚拟的类型来代替(实际上是用一个标识符来占位),等发生函数调用时再根据传入的实参来逆推出真正的类型。这个通用函数就称为函数模板(Function Template)

函数模板的定义

1
2
3
4
template <typename TName1, typename TName2, ...> 返回值类型 函数名(形参列表) 
{
// 函数体中可使用类型参数
}

template 是定义函数模板的关键字,后面紧跟尖括号,尖括号内用于声明类型参数,其中typename也是关键字,可以声明多个类型参数.template 这样的结构被称为模板头,模板头中声明的类型参数可以用在后面函数定义或声明的任何位置.需要注意的是,typename 关键字可以被class关键字替代.

函数模板的调用

在调用函数模板时,可以不用显示的指明具体类型参数,编译器会自动根据实参的类型做类型推断得到模板中的类型参数的类型,这个过程称为模板实参推断。对于模板实参推断,我们要注意函数调用时的类型转换问题。函数模板调用时,参数的类型转换相较于普通函数调用会收到更多限制,仅能进行[const]转换和数组或者函数指针转换,其他都不能应用于模板函数。例如有如下5个函数模板:

1
2
3
4
5
template<typename T> void func1(T a, T b);
template<typename T> void func2(T *buff);
template<typename T> void func3(const T &stu);
template<typename T> void func4(T a);
template<typename T> void func5(T &a);

当按照如下代码进行调用时:

1
2
3
4
5
6
7
8
int name[20];
Student Stu1("XiaoMing", 20, 96.5);

func1(12.5, 30); // Error
func2(name); // T == int
func3(stu1); // T == Student
func4(name); // T == int*
func5(name); // T == int[20]

对于func1的调用由于第一个参数为double类型,第二个为int类型,编译器不知道T应该为哪个类型,并且也不会做自动类型的转换,所以调用出错。

对于func2的调用,name数组会转换为指针。所以T对应的类型为int。

对于func3的调用,stu1参数会从非const转换为const,所以T对用的类型为Student类。

对于func4的调用,name参数会从数组类型int [20]转换为指针int *, 所以T对应的类型为int *;

对于func5的调用,name参数作为引用类型时不会从数组类型转换为指针,仍然为int [20]类型,所以T对应的类型为int [20];

Notice

由于将引用作为函数参数时,数组参数传入函数时并不会转换为指针,所以对于下面的函数模板调用方式就会出错:

1
2
3
4
template <typename T> void func(T &a, T &b);
int a[20];
int b[10];
func(a, b);

在上述调用中,由于传入参数a,b的类型分别为int [20]和int [10],导致编译器不能确定T的类型,从而报错。

类模板

C++中除了函数模板还支持类模板,类模板中定义的类型参数可以用在类的声明和实现中。类模板同样将数据的类型参数化,将类的实现与数据类型分离。声明类模板的方式与函数模板类似,同样是再类声明之前加上一个模板头。

1
2
3
4
template <typename Name1, typename Name2>
class ClassName {
// Class body
};

上面的代码仅仅是模板类的声明,对于在类外定义的成员函数,在定义时也需要加上模板头,类模板的成员函数定义格式如下:

1
2
3
4
5
template <typename T1, typename T2, ...>
ReturnType ClassName<T1, T2, ...>::Func(Args)
{
// Function body
}

与函数模板不同的是,类模板在实例化时必须要显式的指明数据类型,编译器不能根据给定的数据推演出数据类型。

类模板的实例化演示如下:

1
2
3
4
Point<int, int> p1(10, 20);
Point<float, float> p2(10.1, 15.5);

Point<int, float> *p1 = new Point<int, float>(10, 15.5);

上面的代码实例化了3个Point模板类对象。

模板的显式具体化

模板的非类型参数

C++模板中除了支持类型参数,还支持非类型的参数。例如下面的模板头:

1
template<typename T, int N> void func(T (&arr)[N])

上面的模板函数定义中,在模板头中除了类型参数T, 还有一个非类型参数N,它有具体的类型int, 用来传递值。当调用一个函数模板或者一个类模板时,非类型参数会被用户提供,或者编译器推断出的值取代。

模板中非类型参数的类型受到了严格限制,只能是一个整数,或者是一个指向对象或函数的指针(引用)。并且:

  1. 当非类型参数是一个整数时,传递给它的实参或者由编译器推导出的实参必须是一个常量表达式,而不能是变量。
  2. 当非类型参数是一个指针(引用)时, 绑定到该指针的实参必须具有静态生命周期,即实参必须是存在虚拟地址空间中的静态数据区,而不能是栈区或堆区的。

模板的实例化

模板(Templet)并不是真正的函数或类,它仅仅是编译器用来生成函数或类的一张“图纸”。模板不会占用内存,最终生成的函数或者类才会占用内存。由模板生成函数或类的过程叫做模板的实例化(Instantiate),相应地,针对某个类型生成的特定版本的函数或类叫做模板的一个实例(Instantiation)。模板也可以看做是编译器的一组指令,它命令编译器生成我们想要的代码。

模板的实例化是按需进行的,用到哪个类型就生成针对哪个类型的函数或类,不会提前生成过多的代码。也就是说,编译器会根据传递给类型参数的实参(也可以是编译器自己推演出来的实参)来生成一个特定版本的函数或类,并且相同的类型只生成一次。实例化的过程也很简单,就是将所有的类型参数用实参代替。

Notice

通过类模板创建对象时并不会实例化所有的成员函数,只有等到真正调用它们时才会被实例化;如果一个成员函数永远不会被调用,那它就永远不会被实例化。这说明类的实例化是延迟的、局部的,编译器并不着急生成所有的代码。

由于类模板实例化的特殊性,模板的定义并不会直接将模板实例化,而只是提供一个实例化的图纸,因此在多文件编程中,不能像传统模块化那样将模板的声明和实现分别放到头文件和源文件中,这样做可能会导致链接器无法找到实例。而应该把模板的定义也一起放到头文件中,或者使用模板的显示实例化。

通过代码明确地告诉编译器需要针对哪个类型进行实例化,这称为显式实例化。模板的显示实例化语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 函数模板声明
template<typename T1, typename T2> void func(T1 &a, T2 &b);

// 类模板声明
template<typename T1, typename T2> class Point {
public:
Point();
};


//显示实例化一个函数模板:template 返回值 函数名(类型1 a, 类型2 b, ...);
template void func(double &a, double &b);
//显示实例化一个类模板:template class 类名<类型1, 类型2, ...>
template class Point<int, double>;

显式实例化一个类模板时,会一次性实例化该类的所有成员,包括成员变量和成员函数。