- 内容提要
- 前言
- 第 1 章 预备知识
- 第 2 章 开始学习 C++
- 第 3 章 处理数据
- 第 4 章 复合类型
- 第 5 章 循环和关系表达式
- 第 6 章 分支语句和逻辑运算符
- 第 7 章 函数——C++的编程模块
- 第 8 章 函数探幽
- 第 9 章 内存模型和名称空间
- 第 10 章 对象和类
- 第 11 章 使用类
- 第 12 章 类和动态内存分配
- 第 13 章 类继承
- 第 14 章 C++中的代码重用
- 第 15 章 友元、异常和其他
- 第 16 章 string 类和标准模板库
- 第 17 章 输入、输出和文件
- 第 18 章 探讨 C++新标准
- 附录 A 计数系统
- 附录 B C++保留字
- 附录 C ASCII 字符集
- 附录 D 运算符优先级
- 附录 E 其他运算符
- 附录 F 模板类 string
- 附录 G 标准模板库方法和函数
- 附录 H 精选读物和网上资源
- 附录 I 转换为 ISO 标准 C++
- 附录 J 复习题答案
14.4 类模板
继承(公有、私有或保护)和包含并不总是能够满足重用代码的需要。例如,Stack 类(参见第 10 章)和 Queue 类(参见第 12 章)都是容器类(container class),容器类设计用来存储其他对象或数据类型。例如,第 10 章的 Stack 类设计用于存储 unsigned long 值。可以定义专门用于存储 double 值或 string 对象的 Stack 类,除了保存的对象类型不同外,这两种 Stack 类的代码是相同的。然而,与其编写新的类声明,不如编写一个泛型(即独立于类型的)栈,然后将具体的类型作为参数传递给这个类。这样就可以使用通用的代码生成存储不同类型值的栈。第 10 章的 Stack 示例使用 typedef 处理这种需求。然而,这种方法有两个缺点:首先,每次修改类型时都需要编辑头文件;其次,在每个程序中只能使用这种技术生成一种栈,即不能让 typedef 同时代表两种不同的类型,因此不能使用这种方法在同一个程序中同时定义 int 栈和 string 栈。
C++的类模板为生成通用的类声明提供了一种更好的方法(C++最初不支持模板,但模板被引入后,就一直在演化,因此有的编译器可能不支持这里介绍的所有特性)。模板提供参数化(parameterized)类型,即能够将类型名作为参数传递给接收方来建立类或函数。例如,将类型名 int 传递给 Queue 模板,可以让编译器构造一个对 int 进行排队的 Queue 类。
C++库提供了多个模板类,本章前面使用了模板类 valarray,第 4 章介绍了模板类 vector 和 array,而第 16 章将讨论的 C++标准模板库(STL)提供了几个功能强大而灵活的容器类模板实现。本章将介绍如何设计一些基本的特性。
14.4.1 定义类模板
下面以第 10 章的 Stack 类为基础来建立模板。原来的类声明如下:
采用模板时,将使用模板定义替换 Stack 声明,使用模板成员函数替换 Stack 的成员函数。和模板函数一样,模板类以下面这样的代码开头:
关键字 template 告诉编译器,将要定义一个模板。尖括号中的内容相当于函数的参数列表。可以把关键字 class 看作是变量的类型名,该变量接受类型作为其值,把 Type 看作是该变量的名称。
这里使用 class 并不意味着 Type 必须是一个类;而只是表明 Type 是一个通用的类型说明符,在使用模板时,将使用实际的类型替换它。较新的 C++实现允许在这种情况下使用不太容易混淆的关键字 typename 代替 class:
可以使用自己的泛型名代替 Type,其命名规则与其他标识符相同。当前流行的选项包括 T 和 Type,我们将使用后者。当模板被调用时,Type 将被具体的类型值(如 int 或 string)取代。在模板定义中,可以使用泛型名来标识要存储在栈中的类型。对于 Stack 来说,这意味着应将声明中所有的 typedef 标识符 Item 替换为 Type。例如,
应改为:
同样,可以使用模板成员函数替换原有类的类方法。每个函数头都将以相同的模板声明打头:
同样应使用泛型名 Type 替换 typedef 标识符 Item。另外,还需将类限定符从 Stack::改为 Stack<Type>::。例如,
应该为:
如果在类声明中定义了方法(内联定义),则可以省略模板前缀和类限定符。
程序清单 14.13 列出了类模板和成员函数模板。知道这些模板不是类和成员函数定义至关重要。它们是 C++编译器指令,说明了如何生成类和成员函数定义。模板的具体实现——如用来处理 string 对象的栈类——被称为实例化(instantiation)或具体化(specialization)。不能将模板成员函数放在独立的实现文件中(以前,C++标准确实提供了关键字 export,让您能够将模板成员函数放在独立的实现文件中,但支持该关键字的编译器不多;C++11 不再这样使用关键字 export,而将其保留用于其他用途)。由于模板不是函数,它们不能单独编译。模板必须与特定的模板实例化请求一起使用。为此,最简单的方法是将所有模板信息放在一个头文件中,并在要使用这些模板的文件中包含该头文件。
程序清单 14.13 stacktp.h
14.4.2 使用模板类
仅在程序包含模板并不能生成模板类,而必须请求实例化。为此,需要声明一个类型为模板类的对象,方法是使用所需的具体类型替换泛型名。例如,下面的代码创建两个栈,一个用于存储 int,另一个用于存储 string 对象:
看到上述声明后,编译器将按 Stack<Type>模板来生成两个独立的类声明和两组独立的类方法。类声明 Stack<int>将使用 int 替换模板中所有的 Type,而类声明 Stack<string>将用 string 替换 Type。当然,使用的算法必须与类型一致。例如,Stack 类假设可以将一个项目赋给另一个项目。这种假设对于基本类型、结构和类来说是成立的(除非将赋值运算符设置为私有的),但对于数组则不成立。
泛型标识符——例如这里的 Type——称为类型参数(type parameter),这意味着它们类似于变量,但赋给它们的不能是数字,而只能是类型。因此,在 kernel 声明中,类型参数 Type 的值为 int。
注意,必须显式地提供所需的类型,这与常规的函数模板是不同的,因为编译器可以根据函数的参数类型来确定要生成哪种函数:
程序清单 14.14 修改了原来的栈测试程序(程序清单 11.12),使用字符串而不是 unsigned long 值作为订单 ID。
程序清单 14.14 stacktem.cpp
程序清单 14.14 所示程序的运行情况如下:
14.4.3 深入探讨模板类
可以将内置类型或类对象用作类模板 Stack<Type>的类型。指针可以吗?例如,可以使用 char 指针替换程序清单 14.14 中的 string 对象吗?毕竟,这种指针是处理 C-风格字符串的内置方式。答案是可以创建指针栈,但如果不对程序做重大修改,将无法很好地工作。编译器可以创建类,但使用效果如何就因人而异了。下面解释程序清单 14.14 不太适合使用指针栈的原因,然后介绍一个指针栈很有用的例子。
1.不正确地使用指针栈
我们将简要地介绍 3 个试图对程序清单 14.14 进行修改,使之使用指针栈的简单(但有缺陷的)示例。这几个示例揭示了设计模板时应牢记的一些教训,切忌盲目使用模板。这 3 个示例都以完全正确的 Stack<Type>模板为基础:
版本 1 将程序清单 14.14 中的:
替换为:
这旨在用 char 指针而不是 string 对象来接收键盘输入。这种方法很快就失败了,因为仅仅创建指针,没有创建用于保存输入字符串的空间(程序将通过编译,但在 cin 试图将输入保存在某些不合适的内存单元中时崩溃)。
版本 2 将
替换为:
这为输入的字符串分配了空间。另外,po 的类型为 char *,因此可以被放在栈中。但数组完全与 pop( ) 方法的假设相冲突:
首先,引用变量 item 必须引用某种类型的左值,而不是数组名。其次,代码假设可以给 item 赋值。即使 item 能够引用数组,也不能为数组名赋值。因此这种方法失败了。
版本 3 将
替换为:
这为输入的字符串分配了空间。另外,po 是变量,因此与 pop( ) 的代码兼容。然而,这里将会遇到最基本的问题:只有一个 pop 变量,该变量总是指向相同的内存单元。确实,在每当读取新字符串时,内存的内容都将发生改变,但每次执行压入操作时,加入到栈中的的地址都相同。因此,对栈执行弹出操作时,得到的地址总是相同的,它总是指向读入的最后一个字符串。具体地说,栈并没有保存每一个新字符串,因此没有任何用途。
2.正确使用指针栈
使用指针栈的方法之一是,让调用程序提供一个指针数组,其中每个指针都指向不同的字符串。把这些指针放在栈中是有意义的,因为每个指针都将指向不同的字符串。注意,创建不同指针是调用程序的职责,而不是栈的职责。栈的任务是管理指针,而不是创建指针。
例如,假设我们要模拟下面的情况。某人将一车文件夹交付给了 Plodson。如果 Plodson 的收取篮(in-basket)是空的,他将取出车中最上面的文件夹,将其放入收取篮;如果收取篮是满的,Plodson 将取出篮中最上面的文件,对它进行处理,然后放入发出篮(out-basket)中。如果收取篮既不是空的也不是满的,Plodson 将处理收取篮中最上面的文件,也可能取出车中的下一个文件,把它放入收取篮。他采取了自认为是比较鲁莽的行动——扔硬币来决定要采取的措施。下面来讨论他的方法对原始文件处理顺序的影响。
可以用一个指针数组来模拟这种情况,其中的指针指向表示车中文件的字符串。每个字符串都包含文件所描述的人的姓名。可以用栈表示收取篮,并使用第二个指针数组来表示发出篮。通过将指针从输入数组压入到栈中来表示将文件添加到收取篮中,同时通过从栈中弹出项目,并将它添加到发出篮中来表示处理文件。
应考虑该问题的各个方面,因此栈的大小必须是可变的。程序清单 14.15 重新定义了 Stack<Type>类,使 Stack 构造函数能够接受一个可选大小的参数。这涉及到在内部使用动态数组,因此,Stack 类需要包含一个析构函数、一个复制构造函数和一个赋值运算符。另外,通过将多个方法作为内联函数,精减了代码。
程序清单 14.15 stcktp1.h
原型将赋值运算符函数的返回类型声明为 Stack 引用,而实际的模板函数定义将类型定义为 Stack<Type>。前者是后者的缩写,但只能在类中使用。即可以在模板声明或模板函数定义内使用 Stack,但在类的外面,即指定返回类型或使用作用域解析运算符时,必须使用完整的 Stack<Type>。
程序清单 14.16 中的程序使用新的栈模板来实现 Plodson 模拟,它像以前介绍的模拟那样使用 rand( )、srand( ) 和 time( ) 来生成随机数,这里是随机生成 0 和 1,来模拟掷硬币的结果。
程序清单 14.16 stkoptr1.cpp
下面是程序清单 14.16 所示程序的两次运行情况。注意,由于使用了随机特性,每次运行时,文件最后的顺序都可能不同,即使栈大小保持不变。
程序说明
在程序清单 14.16 中,字符串本身永远不会移动。把字符串压入栈实际上是新建一个指向该字符串的指针,即创建一个指针,该指针的值是现有字符串的地址。从栈弹出字符串将把地址值复制到 out 数组中。
该程序使用的类型是 const char *,因为指针数组将被初始化为一组字符串常量。
栈的析构函数对字符串有何影响呢?没有。构造函数使用 new 创建一个用于保存指针的数组,析构函数删除该数组,而不是数组元素指向的字符串。
14.4.4 数组模板示例和非类型参数
模板常用作容器类,这是因为类型参数的概念非常适合于将相同的存储方案用于不同的类型。确实,为容器类提供可重用代码是引入模板的主要动机,所以我们来看看另一个例子,深入探讨模板设计和使用的其他几个方面。具体地说,将探讨一些非类型(或表达式)参数以及如何使用数组来处理继承族。
首先介绍一个允许指定数组大小的简单数组模板。一种方法是在类中使用动态数组和构造函数参数来提供元素数目,最后一个版本的 Stack 模板采用的就是这种方法。另一种方法是使用模板参数来提供常规数组的大小,C++11 新增的模板 array 就是这样做的。程序清单 14.17 演示了如何做。
程序清单 14.17 arraytp.h
请注意程序清单 14.17 中的模板头:
关键字 class(或在这种上下文中等价的关键字 typename)指出 T 为类型参数,int 指出 n 的类型为 int。这种参数(指定特殊的类型而不是用作泛型名)称为非类型(non-type)或表达式(expression)参数。假设有下面的声明:
这将导致编译器定义名为 ArrayTP<double, 12>的类,并创建一个类型为 ArrayTP<double, 12>的 eggweight 对象。定义类时,编译器将使用 double 替换 T,使用 12 替换 n。
表达式参数有一些限制。表达式参数可以是整型、枚举、引用或指针。因此,double m 是不合法的,但 double * rm 和 double * pm 是合法的。另外,模板代码不能修改参数的值,也不能使用参数的地址。所以,在 ArrayTP 模板中不能使用诸如 n++和&n 等表达式。另外,实例化模板时,用作表达式参数的值必须是常量表达式。
与 Stack 中使用的构造函数方法相比,这种改变数组大小的方法有一个优点。构造函数方法使用的是通过 new 和 delete 管理的堆内存,而表达式参数方法使用的是为自动变量维护的内存栈。这样,执行速度将更快,尤其是在使用了很多小型数组时。
表达式参数方法的主要缺点是,每种数组大小都将生成自己的模板。也就是说,下面的声明将生成两个独立的类声明:
但下面的声明只生成一个类声明,并将数组大小信息传递给类的构造函数:
另一个区别是,构造函数方法更通用,这是因为数组大小是作为类成员(而不是硬编码)存储在定义中的。这样可以将一种尺寸的数组赋给另一种尺寸的数组,也可以创建允许数组大小可变的类。
14.4.5 模板多功能性
可以将用于常规类的技术用于模板类。模板类可用作基类,也可用作组件类,还可用作其他模板的类型参数。例如,可以使用数组模板实现栈模板,也可以使用数组模板来构造数组——数组元素是基于栈模板的栈。即可以编写下面的代码:
在最后一条语句中,C++98 要求使用至少一个空白字符将两个>符号分开,以免与运算符>>混淆。C++11 不要求这样做。
1.递归使用模板
另一个模板多功能性的例子是,可以递归使用模板。例如,对于前面的数组模板定义,可以这样使用它:
这使得 twodee 是一个包含 10 个元素的数组,其中每个元素都是一个包含 5 个 int 元素的数组。与之等价的常规数组声明如下:
请注意,在模板语法中,维的顺序与等价的二维数组相反。程序清单 14.18 使用了这种方法,同时使用 ArrayTP 模板创建了一维数组,来分别保存这 10 个组(每组包含 5 个数)的总数和平均值。方法调用 cout.width(2) 以两个字符的宽度显示下一个条目(如果整个数字的宽度不超过两个字符)。
程序清单 14.18 twod.cpp
下面是程序清单 14.18 所示程序的输出。在 twodee 的 10 个元素(每个元素又是一个包含 5 个元素的数组)中,每个元素对应于 1 行:列出了每个元素包含的值、这些值的总和以及平均值。
2.使用多个类型参数
模板可以包含多个类型参数。例如,假设希望类可以保存两种值,则可以创建并使用 Pair 模板来保存两个不同的值(标准模板库提供了类似的模板,名为 pair)。程序清单 14.19 所示的小程序是一个这样的示例。其中,方法 first( ) const 和 second( ) const 报告存储的值,由于这两个方法返回 Pair 数据成员的引用,因此让您能够通过赋值重新设置存储的值。
程序清单 14.19 pairs.cpp
对于程序清单 14.19,需要注意的一点是,在 main( ) 中必须使用 Pair<string, int>来调用构造函数,并将它作为 sizeof 的参数。这是因为类名是 Pair<string, int>,而不是 Pair。另外,Pair<char *, double>是另一个完全不同的类的名称。
下面是程序清单 14.19 所示程序的输出:
3.默认类型模板参数
类模板的另一项新特性是,可以为类型参数提供默认值:
这样,如果省略 T2 的值,编译器将使用 int:
第 16 章将讨论的标准模板库经常使用该特性,将默认类型设置为类。
虽然可以为类模板类型参数提供默认值,但不能为函数模板参数提供默认值。然而,可以为非类型参数提供默认值,这对于类模板和函数模板都是适用的。
14.4.6 模板的具体化
类模板与函数模板很相似,因为可以有隐式实例化、显式实例化和显式具体化,它们统称为具体化(specialization)。模板以泛型的方式描述类,而具体化是使用具体的类型生成类声明。
1.隐式实例化
到目前为止,本章所有的模板示例使用的都是隐式实例化(implicit instantiation),即它们声明一个或多个对象,指出所需的类型,而编译器使用通用模板提供的处方生成具体的类定义:
编译器在需要对象之前,不会生成类的隐式实例化:
第二条语句导致编译器生成类定义,并根据该定义创建一个对象。
2.显式实例化
当使用关键字 template 并指出所需类型来声明类时,编译器将生成类声明的显式实例化(explicit instantiation)。声明必须位于模板定义所在的名称空间中。例如,下面的声明将 ArrayTP<string, 100>声明为一个类:
在这种情况下,虽然没有创建或提及类对象,编译器也将生成类声明(包括方法定义)。和隐式实例化一样,也将根据通用模板来生成具体化。
3.显式具体化
显式具体化(explicit specialization)是特定类型(用于替换模板中的泛型)的定义。有时候,可能需要在为特殊类型实例化时,对模板进行修改,使其行为不同。在这种情况下,可以创建显式具体化。例如,假设已经为用于表示排序后数组的类(元素在加入时被排序)定义了一个模板:
另外,假设模板使用>运算符来对值进行比较。对于数字,这管用;如果 T 表示一种类,则只要定义了 T::operator>( ) 方法,这也管用;但如果 T 是由 const char *表示的字符串,这将不管用。实际上,模板倒是可以正常工作,但字符串将按地址(按照字母顺序)排序。这要求类定义使用 strcmp( ),而不是>来对值进行比较。在这种情况下,可以提供一个显式模板具体化,这将采用为具体类型定义的模板,而不是为泛型定义的模板。当具体化模板和通用模板都与实例化请求匹配时,编译器将使用具体化版本。
具体化类模板定义的格式如下:
早期的编译器可能只能识别早期的格式,这种格式不包括前缀 template<>:
要使用新的表示法提供一个专供 const char *类型使用的 SortedArray 模板,可以使用类似于下面的代码:
其中的实现代码将使用 strcmp( )(而不是>)来比较数组值。现在,当请求 const char *类型的 SortedArray 模板时,编译器将使用上述专用的定义,而不是通用的模板定义:
4.部分具体化
C++还允许部分具体化(partial specialization),即部分限制模板的通用性。例如,部分具体化可以给类型参数之一指定具体的类型:
关键字 template 后面的<>声明的是没有被具体化的类型参数。因此,上述第二个声明将 T2 具体化为 int,但 T1 保持不变。注意,如果指定所有的类型,则<>内将为空,这将导致显式具体化:
如果有多个模板可供选择,编译器将使用具体化程度最高的模板。给定上述三个模板,情况如下:
也可以通过为指针提供特殊版本来部分具体化现有的模板:
如果提供的类型不是指针,则编译器将使用通用版本;如果提供的是指针,则编译器将使用指针具体化版本:
如果没有进行部分具体化,则第二个声明将使用通用模板,将 T 转换为 char *类型。如果进行了部分具体化,则第二个声明将使用具体化模板,将 T 转换为 char。
部分具体化特性使得能够设置各种限制。例如,可以这样做:
给定上述声明,编译器将作出如下选择:
14.4.7 成员模板
模板可用作结构、类或模板类的成员。要完全实现 STL 的设计,必须使用这项特性。程序清单 14.20 是一个简短的模板类示例,该模板类将另一个模板类和模板函数作为其成员。
程序清单 14.20 tempmemb.cpp
在程序清单 14.20 中,hold 模板是在私有部分声明的,因此只能在 beta 类中访问它。beta 类使用 hold 模板声明了两个数据成员:
n 是基于 int 类型的 hold 对象,而 q 成员是基于 T 类型(beta 模板参数)的 hold 对象。在 main( ) 中,下述声明使得 T 表示的是 double,因此 q 的类型为 hold<double>:
blab( ) 方法的 U 类型由该方法被调用时的参数值显式确定,T 类型由对象的实例化类型确定。在这个例子中,guy 的声明将 T 的类型设置为 double,而下述方法调用的第一个参数将 U 的类型设置为 int(参数 10 对应的类型):
因此,虽然混合类型引起的自动类型转换导致 blab( ) 中的计算以 double 类型进行,但返回值的类型为 U(即 int),因此它被截断为 28,如下面的程序输出所示:
注意到调用 guy.blab( ) 时,使用 10.0 代替了 10,因此 U 被设置为 double,这使得返回类型为 double,因此输出为 28.2608。
正如前面指出的,guy 对象的声明将第二个参数的类型设置为 double。与第一个参数不同的是,第二个参数的类型不是由函数调用设置的。例如,下面的语句仍将 blah( ) 实现为 blah(int, double),并根据常规函数原型规则将 3 转换为类型 double:
可以在 beta 模板中声明 hold 类和 blah 方法,并在 beta 模板的外面定义它们。然而,很老的编译器根本不接受模板成员,而另一些编译器接受模板成员(如程序清单 14.20 所示),但不接受类外面的定义。然而,如果所用的编译器接受类外面的定义,则在 beta 模板之外定义模板方法的代码如下:
上述定义将 T、V 和 U 用作模板参数。因为模板是嵌套的,因此必须使用下面的语法:
而不能使用下面的语法:
定义还必须指出 hold 和 blab 是 beta<T>类的成员,这是通过使用作用域解析运算符来完成的。
14.4.8 将模板用作参数
您知道,模板可以包含类型参数(如 typename T)和非类型参数(如 int n)。模板还可以包含本身就是模板的参数,这种参数是模板新增的特性,用于实现 STL。
在程序清单 14.21 所示的示例中,开头的代码如下:
模板参数是 template <typename T>class Thing,其中 template <typename T>class 是类型,Thing 是参数。这意味着什么呢?假设有下面的声明:
为使上述声明被接受,模板参数 King 必须是一个模板类,其声明与模板参数 Thing 的声明匹配:
在程序清单 14.21 中,Crab 的声明声明了两个对象:
前面的 legs 声明将用 King<int>替换 Thing<int>,用 King<double>替换 Thing<double>。然而,程序清单 14.21 包含下面的声明:
因此,Thing<int>将被实例化为 Stack<int>,而 Thing<double>将被实例化为 Stack<double>。总之,模板参数 Thing 将被替换为声明 Crab 对象时被用作模板参数的模板类型。
Crab 类的声明对 Thing 代表的模板类做了另外 3 个假设,即这个类包含一个 push( ) 方法,包含一个 pop( ) 方法,且这些方法有特定的接口。Crab 类可以使用任何与 Thing 类型声明匹配,并包含方法 push( ) 和 pop( ) 的模板类。本章恰巧有一个这样的类——stacktp.h 中定义的 Stack 模板,因此这个例子将使用它。
程序清单 14.21 tempparm.cpp
下面是程序清单 14.21 所示程序的运行情况:
可以混合使用模板参数和常规参数,例如,Crab 类的声明可以像下面这样打头:
现在,成员 s1 和 s2 可存储的数据类型为泛型,而不是用硬编码指定的类型。这要求将程序中 nebula 的声明修改成下面这样:
模板参数 T 表示一种模板类型,而类型参数 U 和 V 表示非模板类型。
14.4.9 模板类和友元
模板类声明也可以有友元。模板的友元分 3 类:
- 非模板友元;
- 约束(bound)模板友元,即友元的类型取决于类被实例化时的类型;
- 非约束(unbound)模板友元,即友元的所有具体化都是类的每一个具体化的友元。
下面分别介绍它们。
1.模板类的非模板友元函数
在模板类中将一个常规函数声明为友元:
上述声明使 counts( ) 函数成为模板所有实例化的友元。例如,它将是类 hasFriend<int>和 HasFriend<string>的友元。
counts( ) 函数不是通过对象调用的(它是友元,不是成员函数),也没有对象参数,那么它如何访问 HasFriend 对象呢?有很多种可能性。它可以访问全局对象;可以使用全局指针访问非全局对象;可以创建自己的对象;可以访问独立于对象的模板类的静态数据成员。
假设要为友元函数提供模板类参数,可以如下所示来进行友元声明吗?
答案是不可以。原因是不存在 HasFriend 这样的对象,而只有特定的具体化,如 HasFriend<short>。要提供模板类参数,必须指明具体化。例如,可以这样做:
为理解上述代码的功能,想想声明一个特定类型的对象时,将生成的具体化:
编译器将用 int 替代模板参数 T,因此友元声明的格式如下:
也就是说,带 HasFriend<int>参数的 report( ) 将成为 HasFriend<int>类的友元。同样,带 HasFriend<double>参数的 report( ) 将是 report( ) 的一个重载版本——它是 Hasfriend<double>类的友元。
注意,report( ) 本身并不是模板函数,而只是使用一个模板作参数。这意味着必须为要使用的友元定义显式具体化:
程序清单 14.22 说明了上面几点。HasFriend 模板有一个静态成员 ct。这意味着这个类的每一个特定的具体化都将有自己的静态成员。count( ) 方法是所有 HasFriend 具体化的友元,它报告两个特定的具体化(HasFriend<int>和 HasFriend<double>)的 ct 的值。该程序还提供两个 report( ) 函数,它们分别是某个特定 HasFriend 具体化的友元。
程序清单 14.22 frnd2tmp.cpp
有些编译器将对您使用非模板友元发出警告。下面是程序清单 14.22 所示程序的输出:
2.模板类的约束模板友元函数
可以修改前一个示例,使友元函数本身成为模板。具体地说,为约束模板友元作准备,要使类的每一个具体化都获得与友元匹配的具体化。这比非模板友元复杂些,包含以下 3 步。
首先,在类定义的前面声明每个模板函数。
然后,在函数中再次将模板声明为友元。这些语句根据类模板参数的类型声明具体化:
声明中的<>指出这是模板具体化。对于 report( ),<>可以为空,因为可以从函数参数推断出如下模板类型参数:
然而,也可以使用:
但 counts( ) 函数没有参数,因此必须使用模板参数语法(<TT>)来指明其具体化。还需要注意的是,TT 是 HasFriendT 类的参数类型。
同样,理解这些声明的最佳方式也是设想声明一个特定具体化的对象时,它们将变成什么样。例如,假设声明了这样一个对象:
编译器将用 int 替换 TT,并生成下面的类定义:
基于 TT 的具体化将变为 int,基于 HasFriend<TT>的具体化将变为 HasFriend<int>。因此,模板具体化 counts<int>( ) 和 report<HasFriendT<int> >( ) 被声明为 HasFriendT<int>类的友元。
程序必须满足的第三个要求是,为友元提供模板定义。程序清单 14.23 说明了这 3 个方面。请注意,程序清单 14.22 包含 1 个 count( ) 函数,它是所有 HasFriend 类的友元;而程序清单 14.23 包含两个 count( ) 函数,它们分别是某个被实例化的类类型的友元。因为 count( ) 函数调用没有可被编译器用来推断出所需具体化的函数参数,所以这些调用使用 count<int>和 coount<double>( ) 指明具体化。但对于 report( ) 调用,编译器可以从参数类型推断出要使用的具体化。使用<>格式也能获得同样的效果:
程序清单 14.23 tmp2tmp.cpp
下面是程序清单 14.23 所示程序的输出:
正如您看到的,counts<double>和 counts<int>报告的模板大小不同,这表明每种 T 类型都有自己的友元函数 count( )。
3.模板类的非约束模板友元函数
前一节中的约束模板友元函数是在类外面声明的模板的具体化。int 类具体化获得 int 函数具体化,依此类推。通过在类内部声明模板,可以创建非约束友元函数,即每个函数具体化都是每个类具体化的友元。对于非约束友元,友元模板类型参数与模板类类型参数是不同的:
程序清单 14.24 是一个使用非约束友元的例子。其中,函数调用 show2(hfi1,hfi2)与下面的具体化匹配:
因为它是所有 ManyFriend 具体化的友元,所以能够访问所有具体化的 item 成员,但它只访问了 ManyFriend<int>对象。
同样,show2(hfd, hfi2) 与下面具体化匹配:
它也是所有 ManyFriend 具体化的友元,并访问了 ManyFriend<int>对象的 item 成员和 ManyFriend<double>对象的 item 成员。
程序清单 14.24 manyfrnd.cpp
程序清单 14.24 所示程序的输出如下:
14.4.10 模板别名(C++11)
如果能为类型指定别名,将很方便,在模板设计中尤其如此。可使用 typedef 为模板具体化指定别名:
但如果您经常编写类似于上述 typedef 的代码,您可能怀疑要么自己忘记了可简化这项任务的 C++功能,要么 C++没有提供这样的功能。C++11 新增了一项功能——使用模板提供一系列别名,如下所示:
这将 arrtype 定义为一个模板别名,可使用它来指定类型,如下所示:
总之,arrtype<T>表示类型 std::array<T, 12>。
C++11 允许将语法 using =用于非模板。用于非模板时,这种语法与常规 typedef 等价:
习惯这种语法后,您可能发现其可读性更强,因为它让类型名和类型信息更清晰。
C++11 新增的另一项模板功能是可变参数模板(variadic template),让您能够定义这样的模板类和模板函数,即可接受可变数量的参数。这个主题将在第 18 章介绍。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论