8 模板

8.1 模板起源

8.1.1 针对具体类型的实现

C和C++语言的静态类型系统,在满足效率与安全性要求的同时,很大程度上也成为阻碍程序员编写通用代码的桎梏。它迫使人们不得不为每一种数据类型编写完全或几乎完全相同的实现,虽然它们在抽象层面上是一致的。

8.1.2 借助参数宏摆脱类型的限制

宏定义只是在预处理器的作用下,针对源代码的文本替换,其本身不具备函数语义。因此借助于参数宏(又名宏函数)可以在某种程度上使程序的编写者摆脱那些源于类型的约束和限制,但同时也丧失了类型的安全性。

8.1.3 让预编译器写代码

利用宏定义构建通用代码的框架,让预处理器将其扩展为针对不同类型的具体版本。将宏的一般性和函数的类型安全性完美地结合起来。

8.2 函数模板

8.2.1 函数模板的定义

8.2.1.1 模板参数的语法形式

模板的类型参数必须用如下形式的语法来声明:

例如:

这里的typename关键字也可以换成class,但建议使用typename。

8.2.1.2 类型参数

可以使用任何标识符作为类型参数的名称,但使用“T”已经成为了一种惯例。类型参数“T”表示的是,调用者调用这个函数时所指定的任意类型。

可以将任何具体的类型实参(基本类型、自定义类型、类类型等)传递给模板的类型形参,前提是所使用的类型实参必须能够满足该模板所需要的操作。比如将一个不支持小于操作符(<)的类型实参,传递给min或max模板的类型形参,将引发编译错误。

8.2.2 函数模板的使用

8.2.2.1 模板实例化

通常而言,并不是把模板编译成一个可以处理任何类型的单一实体,而是针对传递给模板类型形参的每一种类型实参,都从模板产生出一个以具体类型实现的独立实体。这种以具体类型代替类型参数的过程称为模板的实例化。每个以具体类型实现的独立实体都是该模板的一个实例。

函数模板min的三个实例:

函数模板max的三个实例:

8.2.2.2 使用函数模板时才实例化

函数模板的类型实参放在尖括号中,调用实参放在圆括号中,类型实参必须位于函数模板名和调用实参之间:

只要使用函数模板,编译器就会自动引发这样一个实例化过程,程序员并不需要额外地请求对模板实例化。例如:

8.2.2.3 二次编译

每个函数模板事实上都被编译了两次:

只有在第二次编译时,才会真正产生针对具体函数的二进制机器指令,第一次编译仅仅在编译器内部形成的一个用于描述函数模板的内部表示。

8.2.3 函数模板的隐式推断

8.2.3.1 根据调用参数推断模板参数

如果函数模板调用参数的类型相关于该模板的类型参数,那么在调用该函数模板时,即使不显式指明类型实参,编译器也有能力根据调用参数的类型隐式推断出正确的类型实参,以获得与普通函数调用一致的语法表达。例如:

注意在cx和cy中获取最小和最大值的函数模板调用没有使用隐式推断:

而依然使用显式实例化:

因为cx和cy是字符数组,其类型为char*,隐式推断将以char*作为实例化函数模板的类型实参,实例化结果为:

对由字符指针指向的空字符结尾的C风格字符串,使用小于号比较大小,显然是不合适的。

8.2.3.2 不能隐式推断的三种情况

以下三种情况,不能隐式推断,必须显式指定模板参数:

8.2.3.3 隐式推断与缺省值之间的矛盾

函数模板参数的隐式推断与缺省值之间存在矛盾,因此函数模板的模板参数不能带有缺省值。例如:

C++11以后,允许为函数模板的模板参数指定缺省值,但依然会优先选择隐式推断的结果。

8.2.4 函数模板的重载

8.2.4.1 普通函数和函数模板构成重载

普通函数和可实例化为该函数的函数模板构成重载关系。在其它条件都相同的情况下,编译器优先选择普通函数,除非函数模板能够产生具有更好匹配性的函数实例。

8.2.4.2 函数模板不支持隐式类型转换

函数模板的隐式实例化不支持隐式类型转换,但普通函数支持。因此在参数传递过程中如需隐式类型转换,编译器将优先选择普通函数。

8.2.4.3 显式指定空模板参数列表

可以显式指定一个空的模板参数列表,明确告知编译器使用函数模板,但模板参数却由隐式推断决定。即便是在函数模板中选择,编译器也会尽可能选择类型约束性强的版本,即更特殊的版本。

8.2.4.4 保证模板参数与调用参数一致

如果为函数模板显式指定了模板参数,那么所选择的重载版本必须能够保证模板参数与调用参数的类型一致。

8.2.4.5 函数模板内优先选择普通函数

即使是在函数模板的实例化函数中,编译器仍然优先选择普通函数,前提是该普通函数在一次编译时可见。

8.2.4.6 重载函数模板最好只针对参数的个数或具体类型

8.3 类模板

8.3.1 类模板的定义

8.3.1.1 模板参数的语法形式

模板的类型参数必须用如下形式的语法来声明:

例如:

这里的typename关键字也可以换成class,但建议使用typename。

8.3.1.2 类型参数

在类模板的内部,类型参数可以象其它任何具体类型一样,用于成员变量、成员函数、成员类型(内部类型),甚至基类的声明。例如:

8.3.2 类模板的使用

8.3.2.1 类模板的两步实例化

从类模板到对象实际上经历了两个实例化过程

类模板本身并不代表一个确定的类型,既不能用于定义对象,也不能用于声明指针或引用。只有通过模板实参将其实例化为具体类以后,才具备类型语义。

8.3.2.2 调用谁实例化谁

类模板中,只有那些实际被调用的成员函数才会被实例化,即产生二进制代码。某些类型虽然并没有提供类模板所需要的全部功能,但照样可以实例化该类模板,只要不直接或间接调用那些依赖于未提供功能的成员函数即可。

8.3.2.3 类模板参数不支持隐式推断

与函数模板不同,类模板的类型参数不支持隐式推断。

8.3.3 静态成员与递归实例化

8.3.3.1 类模板的静态成员

类模板的静态成员变量,既不是一个对象一份,也不是一个模板一份,而是在该类模板的每个实例化类中,各有一份独立的拷贝,且为该类的所有实例化对象所共享。

8.3.3.2 类模板的递归实例化

类模板的类型实参可以是任何类型,只要该类型能够提供模板所需要的功能即可。

类模板自身的实例化类亦可实例化其自身,谓之递归实例化。通过这种方法可以很容易地构建那些在空间上具有递归特征的数据结构。

8.3.4 类模板的特化

通过特化类模板,可以优化针对某种特定类型的实现,或者克服某种特定类型在实例化类模板时所表现出的不足。

8.3.4.1 全类特化

特化一个类模板可以特化该类模板的所有成员函数,相当于重新写一个针对某种特定类型的具体类。对一个类模板做全类特化:

8.3.4.2 成员特化

类模板除了可以整体进行特化以外,也可以只针对部分成员函数进行特化。对一个类模板做成员特化,特化实现与通用实现共享同一份声明,因此特化版本与通用版本的函数原型,除模板参数以外,必须严格一致。

8.3.5 类模板的局部特化

8.3.5.1 对部分模板参数自行指定

类模板可以被局部特化,即一方面为类模板指定特定的实现,另一方面又允许用户对部分模板参数自行指定。

8.3.5.2 同等程度地特化匹配导致歧义

如果多个局部特化同等程度地匹配某个声明,那么该声明将因二义性而导致歧义错误:

除非有更好的匹配:

8.3.6 类模板参数的缺省值

8.3.6.1 类模板可以带有缺省参数

类模板的模板参数可以带有缺省值,即缺省模板实参。实例化类模板时,如果提供了模板实参则用所提供的模板实参传递给相应的模板形参,如果没有提供模板实参则相应的模板形参取缺省值。

如果类模板的某个模板参数带有缺省值,那么它后面的所有模板参数必须都带有缺省值。例如:

以下情况都是不允许出现的:

8.3.6.2 后面的参数可以引用前面参数

类模板后面参数的缺省值可以引用前面参数的值。例如:

X<int>即等价于X<int, int*>

8.4 非类型模板参数

8.4.1 普通数值作为模板参数

模板的参数并不局限于类型参数,普通数值也可以作为模板的参数,前面不要写typename,而要写具体类型。

类模板的非类型参数与类型参数一样,也可以带有缺省值。

与类模板一样,函数模板也可以带有非类型参数。

函数模板的非类型参数与类型参数一样,也不可以带有缺省值。

8.4.2 非类型模板参数的限制

8.5 模板技巧

8.5.1 typename

8.5.1.1 声明模板参数

声明模板参数:template<typename T> ...,只有在这种语境下,typename关键字才可以和class关键字互换

8.5.1.2 解决嵌套依赖

在第一次编译模板代码时,模板参数的具体类型尚不明确,编译器会把依赖于模板参数的嵌套类型理解为某个类的静态成员变量。因此当它看到代码中使用这样的标识符声明其它变量时,会报告错误,这就叫做嵌套依赖。

typename关键字旨在告诉编译器,所引用的标识符是个类型名,可以声明变量,具体类型等到实例化时再定。

8.5.2 template

8.5.2.1 声明模板

template关键字的典型用法是声明模板

8.5.2.2 依赖模板参数的模板型成员访问

在模板代码中,通过依赖于模板参数的对象、引用或指针,访问其带有模板特性的成员,编译器常常因为无法正确理解模板参数列表的左右尖括号,而报告编译错误。在模板名前面加上template关键字,意在告诉编译器其后的名称是一个模板,编译器就可以正确理解“<>”了。

template关键字旨在告诉编译器,所引用的标识符是个模板,其后尖括号括起的是传递给该模板的模板参数。

8.5.3 子模板访问基模板

在子类模板中直接访问那些依赖于模板参数的基类模板的成员,编译器在第一次编译时,通常会因为基类类型不明确而只在子类和全局作用域中搜索所引用的符号。在子类模板中可以通过作用域限定符,或者显式使用this指针,迫使编译器到基类作用域中搜索所引用的符号。

8.5.4 模板型模板成员

8.5.4.1 模板型成员变量

类模板的成员变量,如果其类型又源自一个类模板的实例化类,那么它就是一个模板型模板成员变量。

8.5.4.2 模板型成员函数

类模板的成员函数,如果除了类模板的模板参数外,还需要其它模板参数,那么它就是一个模板型模板成员函数。

8.5.4.3 模板型成员类型

类模板的成员类型,如果除了类模板的模板参数外,还需要其它模板参数,那么它就是一个模板型模板成员类型。

8.5.5 模板型模板参数

函数模板和类模板的模板型模板参数:

8.5.5.1 函数模板和类模板的模板参数可以是模板

函数模板和类模板的模板参数本身又可以是个模板。

8.5.5.2 模板型模板参数的实例化

可以使用任何类型,甚至其它模板参数,实例化模板型模板参数。

8.5.5.3 模板型模板参数的缺省参数

模板型模板参数的模板参数也可以带有缺省值。

8.5.6 零初始化

8.5.6.1 未初始化的基本类型

基本类型的局部变量,如果没有被显式初始化,其初值通常是不确定的:

类类型的局部变量,如果没有被显式初始化,其初值通常由该类的缺省构造函数决定:

这样就会在包含模板的代码中,表现出某种程度的不一致:

8.5.6.2 显式缺省构造

如果希望模板中所有参数化类型的变量,无论是类类型还是基本类型,都能以缺省方式被初始化,就必须对其进行显式地缺省构造:

当用int实例化该函数模板时:

当用Student实例化该函数模板时:

对类型一致性的追求,始终是模板设计的核心准则之一。

8.5.6.3 在初始化表中显式初始化

对于类模板,可在其缺省构造函数的初始化表中,显式初始化各个成员变量,无论是类类型的还是基本类型的:

当用int实例化该类模板时:

当用Student实例化该类模板时:

类型一致性的设计准则,同样得到了完美的体现。

8.5.7 字符串型函数模板实参

将字符串传递给带有引用型参数和非引用型参数的函数模板,将导致完全不同的结果。函数模板的隐式推断会把引用型参数推断为对整个字符数组的引用,而非引用型参数则仅仅以字符指针的形式接收字符串的首地址。

对同一个函数模板,同时声明引用和非引用两个版本可能会引发歧义。而只使用非引用的版本又会导致无谓的拷贝。如果不想强迫用户通过显式类型转换或者显式实例化,完全用C++的string取代C风格的字符串,那么为非引用版本专门提供一个针对字符指针的重载总是有效的。

8.5.8 类模板的虚成员函数

8.5.8.1 类模板可以定义虚函数

类模板的普通成员函数可以是虚函数,即可以为类模板定义虚成员函数。和普通类的虚成员函数一样,类模板的虚成员函数亦可表现出多态性:

8.5.8.2 类模板的虚函数不能是模板

无论是类还是类模板,其虚成员函数都不能是模板函数。基于虚函数的多态机制,需要一个名为虚函数表的函数指针数组。该数组在类被编译或类模板被实例化的过程中产生,而此时那些模板形式的成员函数尚未被实例化,其入口地址和重载版本的个数,要等到编译器处理完对该函数的所有调用以后才能确定。成员函数模板的延迟编译阻碍了虚函数表的静态构建。

8.5.8.3 实例化类模板时所用的类型实参会影响虚函数覆盖的条件

只有当类模板B中的T,与类模板A中的T,取相同类型时,B中的foo函数才是虚函数,并对A中的foo函数构成覆盖。

8.6 模板实战

8.6.1 模板的编译模型

8.6.1.1 单一模型

将模板的声明、定义和实例化放在单一的编译单元中,无论编译还是链接,其结果总是对的。

8.6.1.2 分离模型

大多数C/C++程序员会这样组织他们的代码:

这样做既能保证所有的类型信息,在整个项目被编译的过程中都可见,避免编译器报告“未声明”错误,同时也不会因为变量和函数定义的重复出现,导致链接器报告“重定义”错误。

模板的定义只是一种规范性的描述,并非真正意义上的类型定义。当编译器看到模板定义时,仅做一般性的语法检查,同时生成一份模板的内部表示,并不生成指令代码。只有当编译器看到模板被实例化为具体函数或具体类时,才会用具体的模板实参,结合之前生成的内部表示,产生二进制指令代码。这个过程被称为模板的后期编译。

每个C或C++源文件都是被单独编译的。编译器在编译模板定义文件时所生成的模板内部表示,此刻早已荡然无存。因此所有基于模板实例的类和函数,编译器只能假设它们被定义在其它模块中,并产生一个指向该(不存在的)定义的引用,期待链接器能在日后解决此问题。但事实上,由于模板的内部表示并没有真正化身为可执行的指令代码,链接器最终报告“未定义的引用”错误。

由此可见,分离模型显然并不适合于包含模板的项目代码。

8.6.1.3 包含模型

把模板定义文件包含在模板声明文件的内部(通常位于声明之后),即让模板的声明和定义同处于一个头文件中。

任何希望使用模板的程序都必须包含模板的声明文件,而模板定义文件亦因包含而内嵌于该声明文件,最终与模板的实例化代码同处一个编译单元。模板的声明、定义与实例化同处一个编译单元中,编译器有能力对模板进行正确地实例化,没有任何链接错误。

包含模型会延长总体的编译时间,而且模板定义文件不得不连同其声明文件一起提供给模板的使用者,向用户暴露出他们不希望或者不应该了解的实现细节。

8.6.1.4 实例模型

在头文件中仅给出模板的声明,不包含模板定义文件。

在模板定义文件中使用实例化指示符,强制编译器在编译该文件时,即根据特定的模板实参对模板进行实例化。

显式实例化指示符是由template关键字和紧接其后的实例化实体(类、全局函数、成员函数等)的声明组成。该声明是一个用实参完全替代形参之后的声明。注意,前面已经显式实例化过的成员不能再次实例化。

根据特定的模板实参对模板进行实例化的工作已在模板定义文件中完成。使用该模板时无需再行实例化,直接引用具体类型,调用具体函数即可。

显式实例化的类型总是有限的,即便只考虑项目中有限类型的情况,也必须仔细跟踪每个需要实例化的类或函数,这对于大型项目而言,将成为十分繁琐的工作。

8.6.1.5 整合包含模型和实例模型

在声明头文件中给出模板的声明:

在定义头文件中给出模板的定义:

在源文件中对模板做显式实例化:

究竟使用包含模型还是实例模型由模板的使用者决定:

8.6.1.6 导出模型

通过export关键字将模板声明为导出,即可在其定义不可见的编译单元中,实例化该模板。

在模板的定义部分不需要再使用export关键字。

编译器对导出型模板的内部表示会有更加持久的记忆,即便该模板的实例化与它的定义不在同一个编译单元中,亦能被正确地编译,不存在任何链接错误。

导出模型在很大程度上增加了编译器的实现难度。截至目前,真正支持export关键字的编译器少之又少(仅Comeau C/C++和Intel 7.x编译器支持),GNU和Microsoft的C++编译器都不支持,而且在C++11以后的标准中,此特性已被废除,export关键字也被移作他用。

8.6.2 预编译头文件

截至目前,包含模型仍然是使用模板的主流策略。每个含有实例化模板代码的源文件,都需要同时包含模板的声明和定义,这部分内容会被重复编译多次,这将导致整个编译时间的延长。预编译头文件的作用就是将那些被多个源文件包含的头文件只被编译一次,并在包含该头文件的编译单元中直接引用编译结果。极大地缩短整体编译时间。

8.6.2.1 编译器的内部状态

当编译一个文件时,编译器从文件的开头一直扫描到文件的结束。对其所见到的每个标记(包括来自头文件的标记),编译器都会更新其内部状态,以反应该标记所表达的语义。编译器的内部状态直接决定了其在目标文件中所产生的代码。

如果有多个需要编译的文件,其前N行代码完全相同,那么编译器在编译第一个文件时,就可以把与这N行代码相对应的内部状态保存在一个文件中。待其编译剩下的文件时,先从这个文件中重新加载事先保存好的内部状态,然后从第N+1行开始编译。从一个文件中加载与N行代码相对应的内部状态,比实际编译N行代码快得多。

8.6.2.2 预编译头文件

多个文件的前N行代码完全相同,这种情况更多地表现为,以相同的顺序包含一组相同的头文件,特别是那些几乎每个程序都会用到的标准头文件,如stdio.h或iostream等。先编译这组头文件,并将编译过程中所形成的内部状态保存在一个专门的文件中,然后在编译所有以包含这组头文件为前N行代码的文件时,直接从这个专门的文件中加载与头文件有关的信息,避免重复编译完全相同的内容,缩短编译时间,提高编译速度。这种编译技术称为预编译头技术。目前大多数C/C++编译器都支持该技术,而那个用于保存编译器内部状态的专门文件则称为预编译头文件(GNU编译器生成的.gch文件或Microsoft编译器生成的.pch文件)。

8.6.2.3 头文件并集

在使用预编译头技术时,所包含的头文件应该是各模块所需头文件的并集。这对于每个具体模块而言可能会有部分冗余,但就整体而言要比只选择有用的头文件,能够获得更快的编译速度。

更一般化的做法是,把那些包括标准头文件在内的,不会或很少修改的头文件及代码集中放在一个头文件中;编译该头文件,得到预编译头文件;然后在每个需要使用其中内容的文件的第一行包含该头文件,编译器会自动为其加载预编译头文件,而不是重新编译该头文件。

8.6.3 浅层实例化

8.6.3.1 诊断信息跟踪所有层次

底层模板往往是在实例化上层模板的过程中被实例化的。因此,一旦因为底层模板的错误而导致上层模板不能被正确地实例化,编译器所给出的诊断信息通常会包含对产生这个问题的所有层次的完整跟踪。程序员往往很难从这么多信息中快速找到症结所在。

8.6.3.2 提前验证模板实参

在程序中增加一些不可能执行到哑代码,只是为了在实例化上层模板时,提前验证一下所用模板实参能否满足底层模板所需要的操作,这种编程技巧称为浅层实例化。

8.6.4 跟踪器

8.6.4.1 测试模板功能和性能

跟踪器是一个用户自定义的类,可以做为测试模板功能和性能的类型实参。

8.6.4.2 跟踪器仅供测试

跟踪器的定义有且仅有满足模板测试的功能。

8.6.4.3 跟踪器的覆盖范围

对于被跟踪模板的每个操作,跟踪器都应该有一个与之对应的跟踪动作。

8.6.5 静态多态

所谓多态,即令同一种类型表现出不同的行为特征。如果把类模板也看作一种类型,为其提供不同的类型实参,即可表现出不同的行为特征,这种多态称为静态多态。与基于虚函数的动态多态相比,静态多态具有更好的时间性能,而且代码也更为简洁。