类模板
继承(公有、私有或保护)和包含并不总是能够满足重用代码的需要。例如,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)提供了几个功能强大而灵活的容器类模板实现。本章将介绍如何设计一些基本的特性。
定义类模板
下面以第10章的 Stack 类为基础来建立模板。原来的类声明如下:
typedef unsigned long Item;class Stack{
private:enum {MAX=10}; // constant specific to classItem items[MAX]; // holds stack itemsint top; // index for top stakc item
public:Stack();bool isemtpy() const;bool isfull() const;// push() returns fasle if stack already is full, true otherwisebool push(const Item & item); // add item to stack// pop() returns false if stack already is empty, true otherwisebool pop(Item & item); // pop top into item
};
采用模板时,将使用模板定义替换 Stack 声明,使用模板成员函数替换 Stack 的成员函数。和模板函数一样,模板类以下面这样的代码开头:
template < class Type>
关键字 template 告诉编译器,将要定义一个模板。尖括号中的内容相当于函数的参数列表。可以把关键字 class 看作是变量的类型名,该变量接受类型作为其值,把 Type 看作是该变量的名称。
这里使用 class 并不意味着 Type 必须是一个类;而只是表明 Type 是一个通用的类型说明符,在使用模板时,将使用实际的类型替换它。较新的C++实现允许在这种情况下使用不太容易混淆的关键字 typename 代替 class:
template<typename Type> // newer choice
可以使用自己的泛型名代替 Type,其命名规则与其他标识符相同。当前流行的选项包括 T 和 Type,我们将使用后者。当模板被调用时,Type 将被具体的类型值(如 int 或 string )取代。在模板定义中,可以使用泛型名称来表示要存储在栈中的类型。对于 Stack 来说,这意味着应将声明中所有的 typedef 标识符 Item 替换为 Type。例如,
Item items[MAX]; // holds stack items
应改为:
Type items[MAX]; // holds stack items
同样,可以使用模板成员函数替换原有类的类方法。每个函数头都将以相同的模板声明打头:
template<class Type>
同样应使用泛型名 Type 替换 typedef 标识符 Item。另外,还需将类限定符从 Stack:: 改为 Stack<Type>::。例如,
bool Stack::push(const Item & item){
...
}
应该为:
template <class Type> // or template <typename Type>
bool Stack<Type>::push(const Type & item){
...
}
如果在类声明中定义了方法(内联定义),则可以省略模板前缀和类限定符。
下面的程序列出了类模板和成员函数模板。知道这些模板不是类和成员函数定义至关重要。它们是 C++ 编译器指令,说明了如何生成类和成员函数定义。模板的具体实现——如用来处理 string 对象的栈类——被称为实例化(instantiation)或具体化(specialization)。不能将模板成员函数放在独立的实现文件中(以前,C++标准确实提供了关键字 export,让您能够将模板成员函数放在独立的实现文件中,但支持该关键字的编译器不多;C++11不再这样使用关键字export,而将其保留用于其他用途)。由于模板不是函数,它们不能单独编译。模板必须与特定的模板实例化请求一起使用。为此,最简单的方法是将所有模板信息放在一个头文件中,并在要使用这些模板的文件中包含该头文件。
// stacktp.h -- a stack template
#ifndef STACKTP_H_
#define STACKTP_H_template<class Type>
class Stack{
private:enum {MAX = 10}; // constant specific to classType items[MAX]; // holds stack itemsint top; // index for top stack item
public:Stack();bool isempty();bool isfull();bool push(const Type & item); // add item to stackbool pop(Type & item); // pop top into item
};template<class Type>
Stack<Type>::Stack(){top = 0;
}template<class Type>
bool Stack<Type>::isempty(){return top == 0;
}template<class Type>
bool Stack<Type>::isfull(){return top==MAX;
}template<class Type>
bool Stack<Type>::push(const Type & item){if(top<MAX){items[top++] = item;return true;}else{return false;}
}template<class Type>
bool Stack<Type>::pop(Type & item){if (top > 0){item = items[--top];return true;}else{return false;}
}#endif
使用模板类
仅在程序包含模板模板不能生成模板类,而必须请求实例化。为此,需要声明一个类型为模板类的对象,方法是使用所需的具体类型替换泛型名。例如,下面的代码创建两个栈,一个用于存储 int,另一个用于存储 string 对象:
Stack<int> kernels; // create a stack of ints
Stack<string> colonels; // create a stack of string objects
看到上述声明后,编译器将按 Stack<Type>模板来生成两个独立的类声明和两组独立的类方法。类声明 Stack<int> 将使用 int 替换模板中所有的 Type,而类声明 Stack<string> 将用 string 替换 Type。当然,使用的算法必须与类型一致。例如,Stack 类假设可以将一个项目赋给另一个项目。这种假设对于基本类型、结构和类来说是成立的(除非将赋值运算符设置为私有的),但对于数组则不成立。
泛型标识符——例如这里的 Type——称为类型参数(type paramet),这意味着它们类似于变量,但赋给它们的不能是数字,而只能是类型。因此,在 kernel 声明中,类型参数 Type 的值为 int。
注意,必须显式地提供所需的类型,这与常规的函数模板是不同的,因为编译器可以根据函数的参数类型来确定要生成哪种函数:
template<class T>
void simple(T t) { cout << t << '\n'; }
...
simple(s); // generate void simple(int)
simple("two") // generate void simple(const char *)
下面的程序修改了原来的栈测试程序,使用字符串而不是 unsigned long 值作为订单 ID。
// stacktem.cpp -- testing the template stack class
#include<iostream>
#include<string>
#include<cctype>#include"14.13_stacktp.h"using std::cin;
using std::cout;int main(){Stack<std::string> st; // create an empty stackchar ch;std::string po;cout << "Please enter A to add a purchase order,\n"<< "P to process a PO, or Q to quit.\n";while(cin>>ch && std::toupper(ch)!='Q'){while ( cin.get() != '\n' )continue;if (!std::isalpha(ch)){cout << '\a';continue;}switch(ch){case 'A':case'a': cout << "Enter a PO number to add: ";cin >> po;if (!st.push(po)){cout << "stack already full\n";}break;case 'P':case 'p': if (!st.pop(po)){cout << "stack already empty\n";}else{cout << "P0 #" << po << " popped\n";}break;}cout << "Please enter A to add a purchase order,\n"<< "P to process a PO, or Q to quit.\n";}cout << "Bye\n";return 0;
}
深入探讨模板类
可以将内置类型或类对象用作类模板 Stack<Type> 的类型。指针可以吗?例如,可以使用 char 指针替换上面程序中的 string 对象吗?毕竟,这种指针是处理 C-风格字符串的内置方式。答案是可以创建指针栈,但如果不对程序做重大修改,将无法很好地工作。编译器可以创建类,但使用效果如何就因人而异了。下面解释上面的程序不太适合使用指针栈的原因,然后介绍一个指针栈很有用的例子。
-
不正确地使用指针栈
我们将简要地介绍 3 个试图对程序进行修改,使之使用指针栈的简单但有缺陷的示例。这几个示例揭示了设计模板时应牢记的一些教训,切记盲目使用模板。这3个示例都以完全正确的 Stack<Type> 模板为基础:Stack<char *> st; // create a stack for pointers-to-char
版本 1 将上面程序中的:
string po;
替换为:
char * po;
这旨在用 char 指针而不是 string 对象来接收键盘输入。这种方法很快就失败了,因为仅仅创建指针,没有创建用于保存输入字符串的空间(程序将通过编译,但在 cin 试图将输入保存在某些不合适的内存单元中时崩溃)。
版本2将
string po;
替换为:
char po[40];
这为输入的字符串分配了空间。另外,po 的类型为 char * ,因此可以被放在栈中。但数组完全与 pop() 方法的假设相冲突:
template <class Type> bool Stack<Type>::pop(Type & item){if (top>0){item = items[--top];return true;}else{return false;} }
首先,引用变量 item 必须引用某种类型的左值,而不是数组名。其次,代码假设可以给 item 赋值。即使 item 能够引用数组,也不能为数组名赋值。因此这种方法失败了。
版本3将string po;
替换为:
char * po = new char[40];
这为输入的字符串分配了空间。另外,po 是变量,因此与 pop() 的代码兼容。然而,这里将会遇到最基本的问题:只有一个 pop 变量,该变量总是指向相同的内存单元。确实,在每当读取新字符串时,内存的内容都将发生改变,但每次执行压入操作时,加入到栈中的地址都相同。因此,对栈执行弹出操作时,得到的地址总是相同的,它总是指向读入的最后一个字符串。具体地说,栈并没偶保存每一个新字符串,因此没有任何用途。
- 正确使用指针栈
使用指针栈的方法之一是,让调用程序提供一个指针数组,其中每个指针都指向不同的字符串。注意,创建不同指针是调用程序的职责,而不是栈的职责。栈的任务是管理指针,而不是创建指针。
例如,假设我们要模拟下面的情况。某人将一车文件夹交付给了 Plodson。如果 Plodson 的收取篮(in-basker)是空的,他将取出车中最上面的文件夹,将其放入收取篮;如果收取篮是满的,Plodson 将取出篮中最上面的文件,对它进行处理,然后放入发出篮(out-basket)中。如果收取篮既不是空的,也不是满的,Plodson 将处理收取篮中最上面的文件,也可能取出车中的下一个文件,把它放入收取篮。他采取了自认为是比较鲁莽的行动——仍硬币来决定要采取的措施。下面来讨论他的方法对原始文件处理顺序的影响。
可以用一个指针数组来模拟这种情况,其中的指针指向表示车中文件的字符串。每个字符串都包含文件所描述的人的姓名。可以用栈表示收取篮,并使用第二个指针数组来表示发出篮。通过将指针从输入数组压入到栈中来表示将文件添加到收取篮中,同时通过从栈中弹出项目,并将它添加到发出篮中来表示处理文件。
应考虑该问题的各个方面,因此栈的大小必须是可变的。下面的程序重新定义了 Stack<Type> 类,使 Stack 构造函数能够接受一个可选大小的参数。这涉及到在内部使用动态数组,因此,Stack 类需要包含一个析构函数、一个复制构造函数和一个赋值运算符。另外,通过将多个方法作为内联函数,精简了代码。 - 正确使用指针栈
// stcktp1.h -- modified Stack template
#ifndef STCKTP1_H_
#define STCKTP1_H_template<class Type>
class Stack {
private:enum {SIZE = 10}; // default sizeint stacksize;Type * items; // holds stack itemsint top; // index for top size item
public:explicit Stack(int ss = SIZE);Stack(const Stack & st);~Stack() { delete [] items; }bool isempty() { return top == 0; }bool isfull() { return top == stacksize; }bool push(const Type & item); // add item to stackbool pop(Type & item); // pop top itemStack & operator=(const Stack & st);
};template<class Type>
Stack<Type>::Stack(int ss) : stacksize(ss), top(0){items = new Type[stacksize];
}template<class Type>
Stack<Type>::Stack(const Stack & st){stacksize = st.stacksize;top = st.top;items = new Type[stacksize];for(int i = 0; i<top; i++)items[i] = st.items[i];
}template<class Type>
bool Stack<Type>::push(const Type & item){if (top<stacksize){items[top++] = item;return true;}else {return false;}
}template<class Type>
bool Stack<Type>::pop(Type & item){if (top>0){item = item[--top];return true;}else{return false;}
}template<class Type>
Stack<Type> & Stack<Type>::operator=(const Stack<Type> &st){if (this == & st){return *this;}delete[]items;stacksize = st.stacksize;top = st.top;items = new Type[stacksize];for(int i=0; i<top; i++){items[i] = st.items[i];}return *this;
}#endif
原型将赋值运算符函数的返回类型声明为 Stack 引用,而实际的模板函数定义将类型定义为 Stack<Type>。前者是后者的缩写,但只能在类中使用。即可以在模板声明或模板函数定义内使用Stack,但在类的外面,即指定返回类型或使用作用域解析运算符时,必须使用完整的 Stack<Type>。
下面的程序使用新的栈模板来实现 Plodson 模拟,它像以前介绍的模拟那样使用 rand()、srand() 和 time() 来生成随机数,这里是随机生成 0 和 1, 来模拟掷硬币的结果。
在上面的程序中,字符串本身永远不会移动。把字符串压入栈实际上是新建一个指向该字符串的指针,即创建一个指针,该指针的值是现有的字符串的地址。从栈弹出字符串将把地址复制到out数组中。
该程序使用的类型是 const char *
,因为指针数组将被初始化为一组字符串常量。
栈的析构函数对字符串有何影响呢?没有。构造函数使用 new 创建一个用于保存指针的数组,析构函数删除该数组,而不是数组元素指向的字符串。
数组模板示例和非类型参数
模板常用作容器类,这是因为类型参数的概念非常适合于将相同的存储方案用于不同的类型。确实,为容器类提供可重用代码是引入模板的主要动机,所以我们来看看另一个例子,深入探讨模板设计和使用的其他几个方面。具体地说,将探讨一些非类型(或表达式)参数以及如何使用数组来处理继承族。
首先介绍一个允许指定数组大小的简单数组模板。一种方法是在类中使用动态数组和构造函数参数来提供元素数目,最后一个版本的 Stack 模板采用的就是这种方法。另一种方法是使用模板参数来提供常规数组的大小,C++11 新增的模板 array 就是这样做的。下面的程序演示了如何做。
// arraytp.h -- Array Template
#ifndef ARRAYTP_H_
#define ARRAYTP_H_#include<iostream>
#include<cstdlib>template <class T, int n>
class ArrayTP{
private:T ar[n];
public:ArrayTP() {};explicit ArrayTP(const T & v);virtual T & operator[](int i);virtual T operator[](int i) const;
};template <class T, int n>
ArrayTP<T,n>::ArrayTP(const T & v){for (int i = 0; i < n; i++){ar[i] = v;}
}template <class T, int n>
T & ArrayTP<T,n>::operator[](int i){if (i < 0 || i >= n){std::cerr << "Error in array limits: " << i<< " is out of range\n";std::exit(EXIT_FAILURE);}return ar[i];
}template <class T, int n>
T ArrayTP<T,n>::operator[](int i) const{if (i < 0 || i >= n){std::cerr << "Error in array limits: " << i<< " is out of range\n";}return ar[i];
}#endif
请注意以上程序的模板头:
template<class T, int n>
关键字 class(或在这种上下文中等价的关键字 typename)指出 T 为类型参数,int 指出 n 的类型为 int。这种参数(指定特殊的类型而不是用作泛型名)称为非类型(non-type)或表达式(expression)参数。假设有下面的声明:
ArrayTP<double, 12> egweights;
这将导致编译器定义名为 ArrayTP<double,12>的类,并创建一个类型为 ArrayTP<double, 12> 的 eggweight 对象。定义类时,编译器将使用 double 替换 T,使用12替换n。
表达式参数有一些限制。表达式参数可以是整型、枚举、引用或指针。因此,double m 是不合法的。但 double * rm 和 double *pm 是合法的。另外,模板代码不能修改参数的值,也不能使用参数的地址。所以,在 ArrayTP 模板中不能使用诸如 n++ 和 &n 等表达式。另外,实例化模板时,用作表达式参数的值必须是常量表达式。
与 Stack 中使用的构造函数方法相比,这种改变数组大小的方法有一个优点。构造函数方法使用的是通过 new 和 delete 管理的堆内存,而表达式参数方法使用的是为自动变量维护的内存栈。这样,执行速度将更快,尤其是在使用了很多小型数组时。
表达式参数方法的主要缺点是,每种数组大小都将生成自己的模板。也就是说,下面的声明将生成两个独立的类声明:
ArrayTP<double, 12> eggweights;
ArrayTP<double, 13> donuts;
但下面的声明只生成一个类声明,并将数组大小信息传递给类的构造函数:
Stack<int> eggs(12);
Stack<int> dunkers(13);
另一个区别是,构造函数方法更通用,这是因为数组大小是作为类成员(而不是硬编码)存储在定义中的。这样可以将一种尺寸的数组赋给另一种尺寸的数组,也可以创建允许数组大小可变的类。
模板多功能性
可以将用于常规类的技术用于模板类。模板类可用作基类,也可用作组件类,还可用作其他模板类的类型参数。例如,可以使用数组模板实现栈模板,也可以使用数组模板来构造数组——数组元素是基于栈模板的栈。即可以编写下面的代码:
template <typename T> // or <class T>
class Array{
private:T entry;...
};template<typename Type>
class GrowArray : public Array<Type> { ... }; // inheritancetemplate <typeame Tp>
class Stack{Array<Tp> ar; // use an Array<> as a component
};
...
Array< Stack<int> > asi; // an array of stacks of int
最最后一条语句中,C++98要求使用至少一个空白字符将两个>符号分开,以免与运算符 >> 混淆。C++11不要求这样做。
-
递归使用模板
另一个模板多功能性的例子是,可以递归使用模板。例如,对于前面的数组模板定义,可以这样使用它:ArrayTP< ArrayTP<int,5>, 10> twodee;
这使得 twodee 是一个包含 10 个元素的数组,其中每个元素都是一个包含5个int元素的数组.与之等价的常规数组声明如下:
int twodee[10][5];
请注意,在模板语法中,维的顺序与等价的二维数组相反。下面的程序使用了这种方法,同时使用 ArrayTP 模板创建了一维数组,来分别保存这 10 个组(每组包含 5 个数)的总数和平均值。方法调用 cout.width(2) 以两个字符的宽度显示下一个条目(如果整个数字的宽度不超过两个字符)。
// arraytp.h -- Array Template #ifndef ARRAYTP_H_ #define ARRAYTP_H_#include<iostream> #include<cstdlib>template <class T, int n> class ArrayTP{ private:T ar[n]; public:ArrayTP() {};explicit ArrayTP(const T & v);virtual T & operator[](int i);virtual T operator[](int i) const; };template <class T, int n> ArrayTP<T,n>::ArrayTP(const T & v){for (int i = 0; i < n; i++){ar[i] = v;} }template <class T, int n> T & ArrayTP<T,n>::operator[](int i){if (i < 0 || i >= n){std::cerr << "Error in array limits: " << i<< " is out of range\n";std::exit(EXIT_FAILURE);}return ar[i]; }template <class T, int n> T ArrayTP<T,n>::operator[](int i) const{if (i < 0 || i >= n){std::cerr << "Error in array limits: " << i<< " is out of range\n";}return ar[i]; }#endif
-
使用多个类型参数
模板可以包含多个类型参数。例如,假设希望类可以保存两种值,则可以创建并使用 Pair 模板来保存两个不同的值(标准模板库提供了类似的模板,名为 pair)。下面的程序是一个这样的示例。其中,方法 first() const 和 second() const 报告存储的值,由于这两个方法返回 Pair 数据成员的引用。因此让您能够通过赋值重新设置存储的值。
// pairs.cpp -- defining and using a Pair template #include<iostream> #include<string>template<class T1, class T2> class Pair{ private:T1 a;T2 b; public:T1 & first();T2 & second();T1 first() const { return a; }T2 second() const { return b; }Pair(const T1 & aval, const T2 & bval) : a(aval), b(bval) {}Pair() {} };template<class T1, class T2> T1 & Pair<T1, T2>::first(){return a; }template<class T1, class T2> T2 & Pair<T1, T2>::second(){return b; }int main(){using std::cout;using std::endl;using std::string;Pair<string, int> ratings[4] = {Pair<string, int>("The Purpled Duck", 5),Pair<string, int>("Jaquie's Frisco Al Rresco", 4),Pair<string, int>("Cafe Souffle", 5),Pair<string, int>("Bertie's Eats", 3)};int joints = sizeof(ratings) / sizeof(Pair<string, int>);cout << "Rating:\t Eatery\n";for (int i = 0; i < joints; i++){cout << ratings[i].second() << ":\t"<< ratings[i].first() << endl;}cout << "Oops! Revised rating:\n";ratings[3].first() = "Bertie's Fab Eats";ratings[3].second() = 6;cout << ratings[3].second() << ":\t "<< ratings[3].first() << endl;return 0; }
-
默认类型模板参数
类模板的另一项新特性是,可以为参数提供默认值:
template <class T1, class T2 = int> class Topo { ... };
这样,如果省略 T2 的值,编译器将使用 int:
Topo<double, double> m1; // T1 is double, T2 is double Topo<double>m2; // T1 is double, T2 is int
第 16 章将讨论的标准模板库经常使用该特性,将默认类型设置为类。
虽然可以为类模板类型参数提供默认值,但不能为函数模板参数提供默认值。然而,可以为非类型参数提供默认值,这对于类模板和函数模板都是适用的。
模板的具体化
类模板与函数模板很相似,因为可以有隐式实例化、显式实例化和显式具体化,它们统称为具体化(specialization)。模板以泛型的方式描述类,而具体化是使用具体的类型生成类声明。
-
隐式实例化
到目前为止,本章所有的模板示例使用的都是隐式实例化(implicit instantiation),即它们声明一个或多个对象,指出所需的类型,而编译器使用通用模板提供的处方生成具体的类定义:ArrayTP<int, 100> stuff; // implicit instantiation
编译器在需要对象之前,不会生成类的隐式实例化:
ArrayTP<double, 30> * pt; // a pointer, no object needed yet pt = new ArrayTP<double, 30>; // now an object is needed
第二条语句导致编译器生成类定义,并根据该定义创建一个对象。
-
显式实例化
当使用关键字 template 并指出所需类型来声明类时,编译器将生成类声明的显式实例化(explicit instantiation)。声明必须位于模板定义所在的名称空间中。例如,下面的声明将 ArrayTP<string, 100> 声明为一个类:template class ArrayTP<string, 100>; // generate ArrayTP<string, 100> class
在这种情况下,虽然没有创建或提及类对象,编译器也将生成类声明(包括方法定义)。和隐式实例化一样,也将根据通用模板来生成具体化。
-
显式具体化
显式具体化(explicit specialiaztion)是特定类型(用于替换模板中的泛型)的定义。有时候,可能需要在为特殊类型实例化时,对模板进行修改,使其行为不同。在这种情况下,可以创建显式具体化。例如,假设已经为用于表示排序后数组的类(元素在加入时被排序)定义了一个模板:template<typename T> class SortedArray {... // details omitted }
两位,假设模板使用>运算符来对值进行比较。对于数字,这管用;如果 T 表示一种类,则只要定义了 T::operator>() 方法,这也管用;但如果T是由 const char * 表示的字符串,这将不管用。实际上,模板倒是可以正常工作,但字符串将按地址(按照字母顺序)排序。这要求类定义使用 strcmp(),而不是>来对值进行比较。在这种情况下,可以提供一个显式模板具体化,这将采用为具体类型定义的模板,而不是为泛型定义的模板。当具体化模板和通用模板都与实例化请求匹配时,编译器将使用具体化版本。
具体化类模板定义的格式如下:
template <> class Classname<specialized-type-name> { ... };
早期的编译器可能只能识别早期的格式,这种格式不包括前缀 template<>:
class Classname<specialized-type-name> { ... };
要使用新的表示法提供一个专供 const char * 类型使用的 SortedArray 模板,可以使用类似于下面的代码:
template <> class SortedArray<const char *>{... // details omitted };
其中的实现代码将使用 strcmp() 而不是> 来比较数组值。现在,当请求 const char * 类型的 SortedArray 模板时,编译器将使用上述专用的定义,而不是通用的模板定义:
SortedArray<int> scores; // use general definition SortedArray<const char *> dates; // use specialized definition
-
部分具体化
C++ 还允许部分具体化(partial specialization),即部分限制模板的通用性。例如,部分具体化可以给类型参数之一指定具体的类型:// general template template <class T1, class T2> class Pair { ... }; // spcialization with T2 set to int template <class T1> class Pair<T1, int> { ... };
关键字 template 后面的 <> 声明的是没有被具体化的类型参数。因此,上述第二个声明将 T2 具体化为 int,但 T1 保持不变。注意,如果指定所有的类型,则 <> 内将为空,这将导致显式具体化:
// specialiaztion with T1 and T2 set to int template<> class Pair <int, int> { ... };
如果有多个模板可供选择,编译器将使用具体化程度最高的模板。给定上述三个模板,情况如下:
Pair<double, double> p1; // use general Pair template Pair<double, int> p2; // use Pair<T1, int> partial specializtion Pair<int, int> p3; // use Pair<int, int> explicit specialization
也可以通过为指针提供特殊版本来部分具体化现有的模板:
template<class T> // general version class Feeb { ... }; template<class T*> // pointer partial specialization class Feeb { ... }; // modified code
如果提供的类型不是指针,则编译器将使用通用版本;如果提供的是指针,则编译器将使用指针具体化版本:
Feeb <char> fb1; // use general Feeb template, T is char Feeb <char *> fb2; // use Feeb T * specialization, T is char
如果没有进行部分具体化,则第二个声明将使用通用模板,将 T 转换为 char* 类型。如果进行了部分具体化,则第二个声明将使用具体化模板,将T转换为char。
部分具体化特性使得能够设置各种限制。例如,可以这样做:
// general template template <class T1, class T2, class T3> class Trio { ... }; // specialization with T3 set to T2 template<class T1, class T2> class Trio<T1,T2,T2> { ... }; // specialzation with T2 and T3 set to T1* template<class T1> class Trio<T1, T1*, T1*> { .. };
给定上述声明,编译器将作出如下选择:
Trio<int, short, char *> t1; // use general template Trio<int, short>; // use Trio<T1, T2, T2> Trio<char, char*, char*> t3; // use Trio<T1, T1*, T1*>
成员模板
模板可用作结构、类或模板类的成员。要完全实现 STL 的设计,必须使用这项特性。下面的程序是一个简短的模板类示例,该模板类将另一个模板类和模板函数作为其成员。
// tempmemb.cpp -- template members#include<iostream>using std::cout;
using std::endl;template<typename T>
class beta{
private:template <typename V> // nested template class memberclass hold{private:V val;public:hold(V v = 0) : val(v) {}void show() const { cout << val << endl; }V Value() const { return val; }};hold<T> q; // template objecthold<int> n; // template object
public:beta( T t, int i) : q(t), n(i) {}template<typename U> // template methodU blab(U u, T t) { return (n.Value()+q.Value())*u / t;}void Show() const { q.show(); n.show(); }
};int main(){beta<double> guy(3.5, 3);cout << "T was set to double\n";guy.Show();cout << "V was set to T, which is double, then V was set to int\n";cout << guy.blab(10, 2.3) << endl;cout << "U was set to int\n";cout << guy.blab(10.0, 2.3) << endl;cout << "U was set to double\n";cout << "Done\n";return 0;
}
在上面的程序中,hold模板是在私有部分声明的,因此只能在 beta 类中访问它。beta 类使用 hold 模板声明了两个数据成员:
hold<T> q; // template object
hold<int> n; // template object
n 是基于 int 类型的 hold 对象,而 q 成员是基于 T 类型(beta模板参数)的hold 对象。在 main() 中,下述声明使得T表示的是 double,因此q的类型为 hold<double>:
beta<double> guy(3.5, 3);
blab() 方法的 U 类型由该方法被调用时的参数值显式确定,T 类型由对象的实例化类型确定。在这个例子中,guy的声明将 T 的类型设置为 double,而下述方法调用的第一个参数将 U 的类型设置为 int(参数10对应的类型):
cout << guy.blab(10, 2.5) << endl;
因此,虽然混合类型引起的自动类型转换导致 blab() 中的计算以 double 类型进行,但返回值的类型为 U(即int),因此它被截断为28,如下面的程序输出所示:
T was set to double
3.5
3
V was set to T, which is double, then V was set to int
28
U was set to int
28.2609
U was set to double
Done
注意到调用 guy.blab() 时,使用 10.0 代替了 10,因此 U 被设置为 double,这使得返回类型为 double,因此输出为 28.2608.
正如前面指出的,guy 对象的声明将第二个参数的类型设置为 double。与第一个参数不同的是,第二个参数的类型不是由函数调用设置的。例如,下面的语句仍将 blab() 实现为 blab(int, double),并根据常规函数原型规则将 3 转换为类型 double:
cout << guy.blab(10,3) << endl;
可以在 beta 模板中声明 hold 类和 blab 方法,并在 beta 模板的外面定义它们。然而,很老的编译器根本不接受模板成员,而另一些编译器接受模板成员,但不接受类外定义。然而,如果所用的编译器接受类外面的定义,则在beta模板之外定义模板方法的代码如下:
template<typename T>
class beta{
private:template <typename V> // declarationclass hold;hold<T> q;hold<int> n;
public:beta( T t, int i) : q(t), n(i) { }template<typename U> // declarationU blab(U u, T t);void Show() const { q.show(); n.show(); }
};// member definition
template <typename T>template<typename V>class beta<T>::hold{private:V val;public:hold(V v = 0) : val(v) { }void show() const { std::cout << val << std::endl; }V Value() const { return val; }};// member definition
template <typename T>template <typename U>U beta<T>::blab(U u, T t){return (n.Value() + q.Value()) * u / t;}
上述定义将 T、V 和 U 用作模板参数。因为模板是嵌套的,因此必须使用下面的语法:
template<typename T>template<typename V>
而不能使用下面的语法:
template<typename T, typename V>
定义还必须指出 hold 和 blab 是 beta<T> 类的成员,这是通过使用作用域解析运算符来完成的。
将模板用作参数
您知道,模板可以包含类型参数(如typename T)和非类型参数(如 int n)。模板还可以包含本身就是模板的参数,这种参数是模板新增的特性,用于实现 STL。
在下面的程序中,开头的代码如下:
template <template <typename T> class Thing>
class Crab{
...
};
模板参数是 template<typename T> class Thing,其中 template<typename T> class 是类型,Thing 是参数。这意味着什么呢?假设有下面的声明:
Crab<King> legs;
为使上述声明被接受,模板参数King必须是一个模板类,其声明与模板参数Thing的声明匹配:
template<typename T>
class King {...
};
在下面的程序中,Crab 的声明声明了两个对象:
Thing<int> s1;
Thing<double> s2;
前面的 legs 声明将用 King<int> 替换 Thing<int>,用King<double> 替换 Thing<double>。然而,下面的程序清单包含下面的声明:
Crab<Stack> nebula;
因此,Thing<int> 将被实例化为 Stack<int>,而 Thing<double> 将被实例化为 Stack<double>。总之,模板参数 Thing 将被替换为声明 Crab 对象时被用作模板参数的模板类型。
Crab 类的声明对 Thing 代表的模板类做了另外 3 个假设,即这个类包含一个 push() 方法,包含一个 pop() 方法,且这些方法有特定的接口。Crab 类可以使用任何与 Thing 类型声明匹配,并包含方法 push() 和 pop() 的模板类。本章恰巧有一个这样的类——stacktp.h 中定义的 Stack 模板,因此这个例子将使用它。
// tempparm.cpp - template as parameters
#include <iostream>
#include"14.13_stacktp.h"template < template <typename T> class Thing>
class Crab{
private:Thing<int> s1;Thing<double> s2;
public:Crab() { };// assume the thing class push() and pop() membersbool push(int a, double x) { return s1.push(a) && s2.push(x); }bool pop(int &a, double & x) { return s1.pop(a) && s2.pop(x); }
};int main(){using std::cout;using std::cin;using std::endl;Crab<Stack> nebula; // Stack must match template <typename T> class thingint ni;double nb;cout << "Enter int double pairs, such as 4 3.5 (0 0 to end):\n";while (cin >> ni >> nb && ni > 0 && nb > 0){if (!nebula.push(ni,nb)){break;}}while (nebula.pop(ni,nb))cout << ni << ", " << nb << endl;cout << "Done.\n";return 0;}
下面是程序的运行情况:
Enter int double pairs, such as 4 3.5 (0 0 to end):
50 22.48
25 33.87
60 19.12
0 0
60, 19.12
25, 33.87
50, 22.48
Done.
可以混合使用模板参数和常规参数,例如,Crab 类的声明可以像下面这样打头:
template<template <typename T> class Thing, typename U, typename V>
class Crab{
private:Thing<U> s1;Thing<V> s2;...
现在,成员 s1 和 s2 可存储的数据类型为泛型,而不是用硬编码指定的类型。这要求将程序中的 nebula 的声明修改成下面这样:
Crab<Stack, int, double> nebula; // T = Stack, U = int, V = double
模板参数T表示一种模板类型,而类型参数U和V表示非模板类型。
模板类和友元
模板类声明也可以有友元。模板的友元分3类:
- 非模板友元;
- 约束(bound)模板友元,即友元的类型取决于类被实例化时的类型;
- 非约束(unbound)模板友元,即友元的所有具体化都是类的每一个具体化的友元。
下面分别介绍它们。
-
模板类的非模板友元函数
在模板类中将一个常规函数声明为友元:template <class T> class HasFriend{ public:friend void counts(); // friend to all HasFriend instantiations... };
上述声明使 counts() 函数成为模板所有实例化的友元。例如,它将是类HasFriend<int>和HasFriend<string>的友元。
counts() 函数不是通过对象调用的(它是友元,不是成员函数),也没有对象参数,那么它如何访问 HasFriend 对象呢?有很多种可能性。它可以访问全局对象;可以使用全局指针访问非全局对象;可以创建自己的对象;可以访问独立于对象的模板类的静态数据成员。
假设要为友元函数提供模板类参数,可以如下所示来进行友元声明吗?
friend void report(HasFriend &); // possible?
答案是不可以。原因是不存在 HasFriend 这样的对象,而只有特定的具体化,如 HasFriend<short> 。要提供模板类参数,必须指明具体化。例如,可以这样做:
template<class T> class HasFriend{friend void report(HasFriend<T> &); // bound template friend... };
也就是说,带 HasFriend<int> 参数的 report() 将成为 HasFriend<int> 类的友元。同样,带 HasFriend<double> 参数的 report() 将是 report() 的一个重载版本——它是 HasFriend<double> 类的友元。
注意,report() 本身并不是模板函数,而只是使用一个模板作参数。这意味着必须为要使用的友元定义显式具体化:
void report(HasFriend<short> &) { ... }; // explicit specialization for short void report(HasFriend<int> & ) { ... }; // explicit specialization for int
下面的程序说明了上面几点。HasFriend 模板有一个静态成员 ct。这意味着这个类的每一个特定的具体化都将有自己的静态成员。count() 方法是所有 HasFriend 具体化的友元,它报告两个特定的具体化(HasFriend<int> 和 HasFriend<double>)的 ct 的值。该程序还提供两个 report() 函数,它们分别是某个特定 HasFriend 具体化的友元。
// frnd2tmp.cpp -- template class with non-template friends#include <ctime> #include<iostream> using std::cout; using std::endl;template<typename T> class HasFriend{ private:T item;static int ct; public:HasFriend(const T & i) : item(i) { ct++; }~HasFriend() { ct--; }friend void counts();friend void report(HasFriend<T> &); // template parameter };// each specialization has its own static data member template<typename T> int HasFriend<T>::ct = 0;// non-template friend to all HasFriend<T> classes void counts(){cout << "int count: " << HasFriend<int>::ct << "; ";cout << "double count: " << HasFriend<double>::ct << endl; }// non-template friend to the HasFriend<int> class void report(HasFriend<int> & hf){cout << "HasFriend<int>: " << hf.item << endl; }// non-template friend to the HasFriend<double class void report(HasFriend<double> & hf){cout << "HasFriend<double>: " << hf.item << endl; }int main(){cout << "No objects declared: ";counts();HasFriend<int>hfil(10);cout <<"After hfil declared: ";counts();HasFriend<int>hfil2(20);cout << "After hfil2 declared: ";counts();HasFriend<double>hfdb(10.5);cout << "After hfdb declared: ";counts();report(hfil);report(hfil2);report(hfdb);return 0;}
编译器对使用非模板友元发出警告:
14.22_frnd2tmp.cpp:17:38: warning: friend declaration 'void report(HasFriend<T>&)' declares a non-template function [-Wnon-template-friend]friend void report(HasFriend<T> &); // template parameter^14.22_frnd2tmp.cpp:17:38: note: (if this is not what you intended, make sure the function template has already been declared and add <> after the function name here)
下面是程序的输出:
No objects declared: int count: 0; double count: 0 After hfil declared: int count: 1; double count: 0 After hfil2 declared: int count: 2; double count: 0 After hfdb declared: int count: 2; double count: 1 HasFriend<int>: 10 HasFriend<int>: 20 HasFriend<double>: 10.5
-
模板类的约束模板友元函数
可以修改前一个示例,使友元函数本身成为模板。具体地说,为约束模板友元作准备,要使类的每一个具体化都获得与友元匹配的具体化。这比非模板友元要复杂些,包含以下 3 步。
首先,在类定义的前面声明每个模板函数。template<typename T> void counts(); template<typename T> void report(T &);
然后,在函数中再次将模板声明为友元。这些语句根据类模板参数的类型声明具体化:
template <typename TT> class HasFriendT{ ...friend void counts<TT>();friend void report<>(HasFriendT<TT> &); };
声明中的<>指出这是模板具体化。对于 report(),<> 可以为空,因为可以从函数参数推断出如下模板类型参数:
HasFriendT<TT>
然而,也可以使用:
report<HasFriendT<TT> > (HasFriendT<TT> &)
但counts() 函数没有参数,因此必须使用模板参数语法(<TT>)来指明其具体化。还需要注意的是,TT 是 HasFriendT 类的参数类型。
同样,理解这些声明的最佳方式也是设想声明一个具体化的对象时,它们将变成什么样。例如,假设声明了这样一个对象:HasFriendT<int> squack;
编译器将用 int 替换 TT,并生成下面的类定义:
class HasFriendT<int>{ ...friend void counts<int>();friend void reports<>(HasFriendT<int> &); };
基于 TT 的具体化将变为 int,基于 HasFriend<TT> 的具体化将变为 HasFriend<int>。因此,模板具体化 counts<int>() 和 report<HasFriendT<int>() 被声明为 HasFriendT<int>类的友元。
程序必须满足的第三个要求是,为友元提供模板定义。下面的程序说明了这3个方面。请注意,上一个程序包含1个count()函数,它是所有 HasFriend 类的友元;而下面的程序包含两个 count() 函数,它们分别是某个被实例化的类类型的友元。因为 count() 函数调用没有可被编译器用来推断出所需具体化的函数参数,所以这些调用使用 count<int> 和 count<double>() 指明具体化。但对于 report() 调用,编译器可以从参数类型推断出要使用的具体化。使用<>格式也能获得同样的效果:
report<HasFriendT<int> >(hfil2); // same as report(hfil2);
// tmp2tmp.cpp -- template friends to a template class #include<iostream> using std::cout; using std::endl;// template prototypes template<typename T> void counts(); template<typename T> void report(T &);// template class template <typename TT> class HasFriendT{ private:TT item;static int ct; public:HasFriendT(const TT & i) : item(i) { ct++; }~HasFriendT() { ct--;}friend void counts<TT>();friend void report<>(HasFriendT<TT> &); };template<typename T> int HasFriendT<T>::ct = 0;// template friend functions definitions template<typename T> void counts(){cout << "template size: " << sizeof(HasFriendT<T>) << ": ";cout << "template counts(): " << HasFriendT<T>::ct << endl; }template<typename T> void report(T & hf){cout << hf.item << endl; }int main(){counts<int>();HasFriendT<int> hfil(10);HasFriendT<int> hfil2(20);HasFriendT<double> hfdb(10.5);report(hfil); // generate report(HasFriendT<int> &)report(hfil2); // generate report(HasFriendT<int> &)report(hfdb); // generate report(HasFriendT<double> &)cout << "counts<int>() output: \n";counts<int>();cout << "counts<double>() output: \n";counts<double>();return 0; }
下面是程序的输出:
template size: 4: template counts(): 0 10 20 10.5 counts<int>() output: template size: 4: template counts(): 2 counts<double>() output: template size: 8: template counts(): 1
正如输出所示,counts<double> 和 counts<int> 报告的模板大小不同,这表明每种 T 类型都有自己的友元函数 count()。
-
模板类的非约束模板友元函数
前一节中的约束模板友元函数是在类外面声明的模板的具体化。int 类具体化获得 int 函数具体化,依此类推。通过在类内部声明模板,可以创建非约束友元函数,即每个函数具体化都是每个类具体化的友元。对于非约束友元,友元模型类型参数与模板类类型参数是不同的:template<typename T> class ManyFriend{ ...template <typename C, typename D> friend void show2(C &, D & ); };
下面的程序是一个使用非约束友元的例子。其中,函数调用 show2(hfi1, hfi2) 与下面的具体化匹配:
void show2<ManyFriend<double> & , ManyFriend<int> &>(ManyFriend<double> & c, ManyFriend<int> & d);
它也是所有 ManyFriend 具体化的友元。并访问了 ManyFriend<int> 对象的 item 成员和 ManyFriend<double> 对象的 item 成员。
// manyfrnd.cpp -- unbound template friend to a template class #include<iostream>using std::cout; using std::endl;template<typename T> class ManyFriend{ private:T item; public:ManyFriend(const T & i) : item(i) { }template <typename C, typename D> friend void show2(C &, D &); };template <typename C, typename D> void show2(C & c, D & d){cout << c.item << ", " << d.item << endl; }int main(){ManyFriend<int> hfil(10);ManyFriend<int> hfil2(20);ManyFriend<double> hfdb(10.5);cout << "hfil, hfil2: ";show2(hfil, hfil2);cout << "hfdb, hfil2: ";show2(hfdb, hfil2);return 0; }
该程序的输出如下:
hfil, hfil2: 10, 20 hfdb, hfil2: 10.5, 20
模板别名(C++11)
如果能为类型指定别名,将很方便,在模板设计中尤其如此。可使用 typedef 为模板具体化指定别名:
// define three typedef aliases
typedef std::array<double, 12> arrd;
typedef std::array<int, 12> arri;
typedef std::array<std::string, 12> arrst;
arrd gallons; // gallons is type std::array<double, 12>
arri days; // days is type std::array<int, 12>
arrst months; // months is type std::array<std::string, 12>
C++11 新增了一项功能——使用模板提供一系列别名,如下所示:
template<tyename T>using arrtype = std::array<T, 12>; // template to create multiple aliases
这将 arrtype 定义为一个模板别名,可使用它来指定类型,如下所示:
arrtype<double> gallons; // gallons is type std::array<double, 12>
arrtype<int> days; // days is type std::array<int, 12>
arrtype<std::string> months; // months is type std::array<std::string, 12>
总之,arrtype<T> 表示类型 std::array<T, 12>。
C++ 11 允许将语法 using = 用于非模板。用于非模板时,这种语法与常规 typedef 等价:
typedef const char * pc1; // typedef syntax
using pc2 = const char *; // using = syntax
typedef const int *(*pa1)[10]; // typedef syntax
using pa2 = const int *(*)[10]; // using = syntax
习惯这种语法后,您可能发现其可读性更强,因为它让类型名和类型信息更清晰。
C++ 新增的另一项模板功能是可变参数模板( variadic template),让您能够定义这样的模板类和模板函数,即可接受可变数量的参数。这个主题将在 18 章介绍。