文章目录
1. 类模板
继承(公有、私有或保护)和包含并不总是能够满足重用代码的需要。例如, 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)提供了几个功能强大而灵活的容器类模板实现。
本章将介绍如何设计一些基本的特性。
2. 定义类模板
下面以第10章的Stack类为基础来建立模板,原来的类声明如下:
typedef unsigned long Item;
class Stack
{
private:
enum ( MAX = 10]; // constant specific to clas
Item items [MAX]; //holds stack items
int top; //index for top stack iter
public:
Stack();
bool isempty()const;
bool isfull() const;
// push() returns false if stack already is full, true otherwise
bool push(const Item & item); //add item to stack
// pop() returns false if stack already is empty, true otherwise
bool 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)
如果在类声明中定义了方法(内联定义),则可以省略模板前缀和类限定符。
程序清单14.13列出了类模板和成员函数模板。知道这些模板不是类和成员函数定义至关重要。它们是C++编译器指令,说明了如何生成类和成员函数定义。
模板的具体实现一一如用来处理 string对象的栈类一一被称为实例化( Instantiation)或具体化( specialization)。
不能将模板成员函数放在独立的实现文件中(以前,C++标准确实提供了关键字 export,让您能够将模板成员函数放在独立的实现文件中,但支持该关键字的编译器不多;C++11不再这样使用关键字 export,而将其保留用于其他用途)。
由于模板不是函数,它们不能单独编译。模板必须与特定的模板实例化请求一起使用。为此,最简单的方法是将所有模板信息放在一个头文件中,并在要使用这些模板的文件中包含该头文件。
程序清单14.13 stacktp.h
/*
author:梦悦foundation
公众号:梦悦foundation
可以在公众号获得源码和详细的图文笔记
*/
// stacktp.h -- a stack template
#ifndef STACKTP_H_
#define STACKTP_H_
template <class Type>
class Stack
{
private:
enum {
MAX = 10}; // constant specific to class
Type items[MAX]; // holds stack items
int top; // index for top stack item
public:
Stack();
bool isempty();
bool isfull();
bool push(const Type & item); // add item to stack
bool 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
3. 使用模板类
仅在程序包含模板并不能生成模板类,而必须请求实例化。为此,需要声明一个类型为模板类的对象,方法是使用所需的具体类型替换泛型名。
例如,下面的代码创建两个栈,一个用于存储int,另一个用于存储 string对象
Stacks<int> kernels; // create a stack of ints
Stack<string> colonels; // create a stack of string obiects
看到上述声明后,编译器将按 Stack<Type>
模板来生成两个独立的类声明和两组独立的类方法。
类声明 Stack<int>
将使用int 替换模板中所有的Type,而类声明 Stack< string>将用 string替换Type。
泛型标识符一例如这里的Type一称为类型参数( type parameter),这意味着它们类似于变量,但赋给它们的不能是数字,而只能是类型。因此,在 kernel声明中,类型参数Type的值为int.
注意,必须显式地提供所需的类型,这与常规的函数模板是不同的,因为编译器可以根据函数的参数类型来确定要生成哪种函数:
template <class T>
void simple(T t) {
cout << t << '\n';}
simple(2); //生成一个 void simple(int)
simple("two"); //生成一个 void simple(const char *)
程序清单14.14修改了原来的栈测试程序(程序清单1.2,使用字符串而不是 unsigned long 值作为订单ID。
程序清单14.14 stacktem.cpp
/*
author:梦悦foundation
公众号:梦悦foundation
可以在公众号获得源码和详细的图文笔记
*/
// stacktem.cpp -- testing the template stack class
#include <iostream>
#include <string>
#include <cctype>
#include "stacktp.h"
using std::cin;
using std::cout;
using std::endl;
int main()
{
Stack<std::string> st; // create an empty stack
char ch;
std::string po;
cout << "---------------开始--->公众号:梦悦foundation---------------" << endl;
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.isfull())
cout << "stack already full\n";
else
st.push(po);
break;
case 'P':
case 'p': if (st.isempty())
cout << "stack already empty\n";
else {
st.pop(po);
cout << "PO #" << 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";
cout << "---------------结束--->公众号:梦悦foundation---------------" << endl;
// cin.get();
// cin.get();
return 0;
}
程序的运行结果:
meng-yue@ubuntu:~/MengYue/c++/code_reuse/04$ g++ -o stacktem stacktem.cpp
meng-yue@ubuntu:~/MengYue/c++/code_reuse/04$ ./stacktem
---------------开始--->公众号:梦悦foundation---------------
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
a
Enter a PO number to add: meng
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
a
Enter a PO number to add: yue
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
a
Enter a PO number to add: foundation
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
p
PO #foundation popped
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
p
PO #yue popped
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
p
PO #meng popped
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
p
stack already empty
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
p
stack already empty
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
q
Bye
---------------结束--->公众号:梦悦foundation---------------
meng-yue@ubuntu:~/MengYue/c++/code_reuse/04$
4. 深入探讨模板类
可以将内置类型或类对象用作类模板 Stack<Type>
的类型。指针可以吗?例如,可以使用char指针替换程序清单14.14中的 string对象吗?毕竟,这种指针是处理C-风格字符串的内置方式。
答案是可以创建指针栈,但如果不对程序做重大修改,将无法很好地工作。编译器可以创建类,但使用效果如何就因人而异了。下面解释程序清单14.14不太适合使用指针栈的原因,然后介绍一个指针栈很有用的例子。
1.不正确地使用指针栈
我们将简要地介绍3个试图对程序清单14.14进行修改,使之使用指针栈的简单(但有缺陷的)示例。
这几个示例揭示了设计模板时应牢记的一些教训,切忌盲目使用模板。这3个示例都以完全正确的Stack<Type>
模板为基础
Stack<char *> st; // create a stack for pointers-to-char
版本1将程序清单14.14中的:
string po;
替换为:
char * po;
这旨在用char指针而不是 strung对象来接收键盘输入。这种方法很快就失败了,因为仅仅创建指针,没有创建用于保存输入字符串的空间(程序将通过编译,但在cin试图将输入保存在某些不合适的内存元中时崩溃)。
stacktem1.cpp程序运行结果:
meng-yue@ubuntu:~/MengYue/c++/code_reuse/04$ ./stacktem1
---------------开始--->公众号:梦悦foundation---------------
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
a
Enter a PO number to add: meng
Segmentation fault (core dumped)
meng-yue@ubuntu:~/MengYue/c++/code_reuse/04$
版本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能够引用数组,也不能为数组名赋值。因此这种方法失败了.
stacktem2.cpp程序运行结果:
meng-yue@ubuntu:~/MengYue/c++/code_reuse/04$ g++ -o stacktem2 stacktem2.cpp
stacktem2.cpp: In function ‘int main()’:
stacktem2.cpp:46:36: error: cannot bind non-const lvalue reference of type ‘char*&’ to an rvalue of type ‘char*’
st.pop(po);
^
In file included from stacktem2.cpp:11:0:
stacktp.h:56:6: note: initializing argument 1 of ‘bool Stack<Type>::pop(Type&) [with Type = char*]’
bool Stack<Type>::pop(Type & item)
^~~~~~~~~~~
meng-yue@ubuntu:~/MengYue/c++/code_reuse/04$
版本3将
string po;
替换为:
char *po = new char[40];
这为输入的字符串分配了空间。另外,po是变量,因此与pop()的代码兼容。然而,这里将会遇到最基本问题:只有一个pop变量,该变量总是指向相同的内存单元。确实,在每当读取新字符串时,内存的内容都将发生改变,但每次执行压入操作时,加入到栈中的的地址都相同。
因此,对栈执行弹出操作时,得到的地址总是相同的,它总是指向读入的最后一个字符串。具体地说,栈并没有保存每一个新字符串,因此没有任何用途。
stacktem3.cpp程序运行结果:
meng-yue@ubuntu:~/MengYue/c++/code_reuse/04$ ./stacktem3
---------------开始--->公众号:梦悦foundation---------------
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
a
Enter a PO number to add: meng
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
a
Enter a PO number to add: yue
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
a
Enter a PO number to add: foundation
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
p
PO #foundation popped
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
p
PO #foundation popped
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
p
PO #foundation popped
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
q
Bye
---------------结束--->公众号:梦悦foundation---------------
2. 正确使用指针栈
使用指针栈的方法之一是,让调用程序提供一个指针数组,其中每个指针都指向不同的字符串。把些指针放在栈中是有意义的,因为每个指针都将指向不同的字符串。
注意,创建不同指针是调用程序的取责,而不是栈的职责。栈的任务是管理指针,而不是创建指针。
例如,假设我们要模拟下面的情况。某人将一车文件夹交付给了Plodson。如果 Plodson的收取(in-basket)是空的,他将取出车中最上面的文件夹,将其放入收取篮;如果收取篮是满的, Plodson将取出篮中最上面的文件,对它进行处理,然后放入发出篮( out-basket)中。如果收取篮既不是空的也不是满的, Plodson将处理收取篮中最上面的文件,也可能取出车中的下一个文件,把它放入收取篮。
他采取了自认为是比较鲁莽的行动——扔硬币来决定要采取的措施,下面来讨论他的方法对原始文件处理顺序的影响。
可以用一个指针数组来模找这种情况,其中的指针指向表示车中文件的字符串。每个字符串都包含文件所描述的人的姓名。可以用栈表示收取篮,并使用第二个指针数组来表示发出篮。通过将指针从输入数组压入到栈中来表示将文件添加到收取篮中,同时通过从栈中弹出项目,并将它添加到发出篮中来表示处理文件.
应考虑该问题的各个方面,因此栈的大小必须是可变的。程序清单14.15重新定义了 Stack<Type>
类, 使 Stack构造函数能够接受一个可选大小的参数。这涉及到在内部使用动态数组,因此, Stack类需要包含一个析构函数、一个复制构造函数和一个赋值运算符。另外,通过将多个方法作为内联函数,精减了代码.
程序清单14.15 stcktp1.h
/*
author:梦悦foundation
公众号:梦悦foundation
可以在公众号获得源码和详细的图文笔记
*/
// stcktp1.h -- modified Stack template
#ifndef STCKTP1_H_
#define STCKTP1_H_
template <class Type>
class Stack
{
private:
enum {
SIZE = 10}; // default size
int stacksize;
Type * items; // holds stack items
int top; // index for top stack 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 stack
bool pop(Type & item); // pop top into item
Stack & 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 = items[--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>
。
程序清单14.16中的程序使用新的栈模板来实现 Plodson模拟,它像以前介绍的模拟那样使用rand(), srand()和time()来生成随机数,这里是随机生成0和1,来模拟掷硬币的结果。
程序清单14.16 stkoptr1.cpp
/*
author:梦悦foundation
公众号:梦悦foundation
可以在公众号获得源码和详细的图文笔记
*/
// stkoptr1.cpp -- testing stack of pointers
#include <iostream>
#include <cstdlib> // for rand(), srand()
#include <ctime> // for time()
#include "stcktp1.h"
const int Num = 10;
int main()
{
std::srand(std::time(0)); // randomize rand()
std::cout << "Please enter stack size: ";
int stacksize;
std::cin >> stacksize;
// create an empty stack with stacksize slots
Stack<const char *> st(stacksize);
// in basket
const char * in[Num] = {
" 1: Hank Gilgamesh", " 2: Kiki Ishtar",
" 3: Betty Rocker", " 4: Ian Flagranti",
" 5: Wolfgang Kibble", " 6: Portia Koop",
" 7: Joy Almondo", " 8: Xaverie Paprika",
" 9: Juan Moore", "10: Misha Mache"
};
// out basket
const char * out[Num];
int processed = 0;
int nextin = 0;
while (processed < Num)
{
if (st.isempty())
st.push(in[nextin++]);
else if (st.isfull())
st.pop(out[processed++]);
else if (std::rand() % 2 && nextin < Num) // 50-50 chance
st.push(in[nextin++]);
else
st.pop(out[processed++]);
}
for (int i = 0; i < Num; i++)
std::cout << out[i] << std::endl;
std::cout << "Bye\n";
// std::cin.get();
// std::cin.get();
return 0;
}
程序运行的结果:
book@book-desktop:~/meng-yue/c++/code_reuse/04$ ./stckoptr1
Please enter stack size: 10
2: Kiki Ishtar
3: Betty Rocker
1: Hank Gilgamesh
4: Ian Flagranti
5: Wolfgang Kibble
7: Joy Almondo
6: Portia Koop
8: Xaverie Paprika
9: Juan Moore
10: Misha Mache
Bye
book@book-desktop:~/meng-yue/c++/code_reuse/04$ ./stckoptr1
Please enter stack size: 20
1: Hank Gilgamesh
5: Wolfgang Kibble
6: Portia Koop
4: Ian Flagranti
3: Betty Rocker
2: Kiki Ishtar
7: Joy Almondo
10: Misha Mache
9: Juan Moore
8: Xaverie Paprika
Bye
book@book-desktop:~/meng-yue/c++/code_reuse/04$ ./stckoptr1
Please enter stack size: 20
1: Hank Gilgamesh
2: Kiki Ishtar
4: Ian Flagranti
6: Portia Koop
9: Juan Moore
10: Misha Mache
8: Xaverie Paprika
7: Joy Almondo
5: Wolfgang Kibble
3: Betty Rocker
Bye
book@book-desktop:~/meng-yue/c++/code_reuse/04$
程序说明
在程序清单14.16中,字符串本身永远不会移动。把字符串压入栈实际上是新建一个指向该字符串的指针,即创建一个指针,该指针的值是现有字符串的地址。
从栈弹出字符串将把地址值复制到out数组中。该程序使用的类型是 const char*,因为指针数组将被初始化为一组字符串常量。
栈的析构函数对字符串有何影响呢?没有。构造函数使用new创建一个用于保存指针的数组,析构函数删除该数组,而不是数组元素指向的字符串.
5. 数组模板示例和非类型参数
模板常用作容器类,这是因为类型参数的概念非常适合于将相同的存储方案用于不同的类型。确实,为容器类提供可重用代码是引入模板的主要动机,所以我们来看看另一个例子,深入探讨模板设计和使用的其他几个方面。
具体地说,将探讨一些非类型(或表达式)参数以及如何使用数组来处理继承族。
首先介绍一个允许指定数组大小的简单数组模板。一种方法是在类中使用动态数组和构造函数参数来提供元素数目,最后一个版本的Stack模板采用的就是这种方法。
//Stack 模板类的声明
template <typename Type>
class Stack
{
private:
enum {
SIZE = 10}; // default size
int stacksize;
Type * items; // holds stack items
int top; // index for top stack item
public:
explicit Stack(int ss = SIZE);
}
// 构造函数使用 动态数组,可以实现数组大小通过参数指定
template <class Type>
Stack<Type>::Stack(int ss) : stacksize(ss), top(0)
{
items = new Type [stacksize];
}
另一种方法是使用模板参数来提供常规数组的大小,C++ 11新增的模板array就是这样做的。程序清单14.17演示了如何做。
程序清单14.17 arraytp.h
/*
author:梦悦foundation
公众号:梦悦foundation
可以在公众号获得源码和详细的图文笔记
*/
//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";
std::exit(EXIT_FAILURE);
}
return ar[i];
}
#endif
请注意程序清单14.17中的模板头:
template <class T, int n>
关键字 class(或在这种上下文中等价的关键字 typename)指出T为类型参数,int指出n的类型为int。
这种参数(指定特殊的类型而不是用作泛型名)称为非类型( non-type)或表达式( expression)参数。假设有下面的声明:
ArrayTP<double, 12> eggweights;
这将导致编译器定义名为 ArrayTP< double,12>的类,并创建一个类型为 ArrayTP<double,12>的eggweight对象。定义类时,编译器将使用 double替换T,使用12替换n。
表达式参数有一些限制。表达式参数可以是整型、枚举、引用或指针。因此, double m是不合法的,
但 double*m
和 double *pm
是合法的。另外,模板代码不能修改参数的值,也不能使用参数的地址。所以,在 ArrayTP模板中不能使用诸如n++和&n等表达式。另外,实例化模板时,用作表达式参数的值必须是常量表达式。
与 Stack中使用的构造函数方法相比,这种改变数组大小的方法有一个优点。构造函数方法使用的是通过new和 delete管理的堆内存,而表达式参数方法使用的是为自动变量维护的内存栈。这样,执行速度将更快,尤其是在使用了很多小型数组时。
表达式参数方法的主要缺点是,每种数组大小都将生成自己的模板。也就是说,下面的声明将生成两个独立的类声明:
ArrayTP<double, 12> eggweights;
ArrayTP<double, 13> donuts;
即一个是ArrayTP<double, 12> eggweights;
:会生成一个ArrayTP<double, 12>
类声明
class ArrayTP<double, 12>
{
private:
double ar[12];
public:
ArrayTP() {
};
explicit ArrayTP(const double & v);
virtual double & operator[](int i);
virtual double operator[](int i) const;
}
另外一个是:
ArrayTP<double, 13> donuts;
会生成一个ArrayTP<double, 13>
类声明
class ArrayTP<double, 13>
{
private:
double ar[13];
public:
ArrayTP() {
};
explicit ArrayTP(const double & v);
virtual double & operator[](int i);
virtual double operator[](int i) const;
}
这是两个不同的类声明,虽然大部分地方都是一样的。
但下面的声明只生成一个类声明,并将数组大小信息传递给类的构造函数:
Stack<int> eggs(12);
Stack<int> dunkers(13);
另一个区别是,构造函数方法更通用,这是因为数组大小是作为类成员(而不是硬编码)存储在定义中的。
这样可以将一种尺寸的数组赋给另一种尺寸的数组,也可以创建允许数组大小可变的类。
6. 模板多功能性
可以将用于常规类的技术用于模板类。模板类可用作基类,也可用作组件类,还可用作其他模板的类型参数。例如,可以使用数组模板实现栈模板,也可以使用数组模板来构造数组一一数组元素是基于栈模板的栈。即可以编写下面的代码:
template <typename T> // or <class T>
class Array
{
private:
T entry;
};
template <typename Type>
class GrowArray : public Array<Type> //inheritance
{
};
template <typename Type>
class Stack
{
Array<Type> ar;// use an Array<> as a component ar是一个 Array<int> 类,有一个int成员 entry
};
//这句话相当于 entry 是 Stack<int> 类型
Array< Stack<int> > asi; //an array of int Stack
在最后一条语句中,C++98要求使用至少ー个空白字符将两个>符号分开,以免与运算符>混淆。C++11不要求这样做。
7. 递归使用模板
另一个模板多功能性的例子是,可以递归使用模板。例如,对于前面的数组模板定义,可以这样使用它
class ArrayTP
{
private:
T ar[n];
};
//ArrayTP<int, 5> 相当于是 int a[5]; 的数组
ArrayTP< ArrayTP<int, 5>, 10> twodee; //相当于 ArrayTP<int a[5], 10> 就相当于 a[10] 且每一个成员都是一个 int [5]的数组。
这使得 twodee是一个包含10个元素的数组,其中每个元素都是一个包含5个int元素的数组。与之等价的常规数组声明如下:
int twodee [10][5];
请注意,在模板语法中,维的顺序与等价的二维数组相反。程序清单14.18使用了这种方法,同时使用 ArrayTP模板创建了一维数组,来分别保存这10个组(每组包含5个数)的总数和平均值。
方法调用cout.width(2)
以两个字符的宽度显示下一个条目(如果整个数字的宽度不超过两个字符)
程序清单14.18 twod.cpp
/*
author:梦悦foundation
公众号:梦悦foundation
可以在公众号获得源码和详细的图文笔记
*/
// twod.cpp -- making a 2-d array
#include <iostream>
#include "arraytp.h"
int main(void)
{
using std::cout;
using std::endl;
ArrayTP<int, 10> sums;
ArrayTP<double, 10> aves;
ArrayTP< ArrayTP<int,5>, 10> twodee;
int i, j;
for (i = 0; i < 10; i++)
{
sums[i] = 0;
for (j = 0; j < 5; j++)
{
twodee[i][j] = (i + 1) * (j + 1);
sums[i] += twodee[i][j];
}
aves[i] = (double) sums[i] / 10;
}
for (i = 0; i < 10; i++)
{
for (j = 0; j < 5; j++)
{
cout.width(2);
cout << twodee[i][j] << ' ';
}
cout << ": sum = ";
cout.width(3);
cout << sums[i] << ", average = " << aves[i] << endl;
}
cout << "Done.\n";
// std::cin.get();
return 0;
}
程序运行的结果:
book@book-desktop:~/meng-yue/c++/code_reuse/04$ ./twod
1 2 3 4 5 : sum = 15, average = 1.5
2 4 6 8 10 : sum = 30, average = 3
3 6 9 12 15 : sum = 45, average = 4.5
4 8 12 16 20 : sum = 60, average = 6
5 10 15 20 25 : sum = 75, average = 7.5
6 12 18 24 30 : sum = 90, average = 9
7 14 21 28 35 : sum = 105, average = 10.5
8 16 24 32 40 : sum = 120, average = 12
9 18 27 36 45 : sum = 135, average = 13.5
10 20 30 40 50 : sum = 150, average = 15
Done.
book@book-desktop:~/meng-yue/c++/code_reuse/04$
8. 使用多个类型参数
模板可以包含多个类型参数。
例如,假设希望类可以保存两种值,则可以创建并使用Pair模板来保存两个不同的值(标准模板库提供了类似的模板,名为pair)。
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() {
}
};
程序清单14.19所示的小程序是一个这样的示例。其中,方法 first() const和 second() const报告存储的值,由于这两个方法返回Pair数据成员的引用,因此让您能够通过赋值重新设置存储的值。
程序清单14.19 pairs.cpp
/*
author:梦悦foundation
公众号:梦悦foundation
可以在公众号获得源码和详细的图文笔记
*/
// 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 Fresco", 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;
// std::cin.get();
return 0;
}
对于程序清单14.19,需要注意的一点是,在main()中必须使用 Pair<string, int>
来调用构造函数,并将它作为 sizeof的参数。这是因为类名是 Pair<string, int>
,而不是Pair。另外, Pair<char, double>
是另一个完全不同的类的名称。
下面是程序清单14.19所示程序的输出:
程序运行结果:
book@book-desktop:~/meng-yue/c++/code_reuse/04$ g++ -o pairs pairs.cpp
book@book-desktop:~/meng-yue/c++/code_reuse/04$ ./pairs
Rating: Eatery
5: The Purpled Duck
4: Jaquie's Frisco Al Fresco
5: Cafe Souffle
3: Bertie's Eats
Oops! Revised rating:
6: Bertie's Fab Eats
book@book-desktop:~/meng-yue/c++/code_reuse/04$
9. 模板参数设置默认类型,默认参数
类模板的另一项新特性是,可以为类型参数提供默认值:
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章将讨论的标准模板库经常使用该特性,将默认类型设置为类。
虽然可以为类模板类型参数提供默认值,但不能为函数模板参数提供默认值。然而,可以为非类型参数提供默认值,这对于类模板和函数模板都是适用的。
10. 模板的具体化
类模板与函数模板很相似,因为可以有隐式实例化、显式实例化和显式具体化,它们统称为具体化( specialization)。模板以泛型的方式描述类,而具体化是使用具体的类型生成类声明。
1.隐式实例化
到目前为止,本章所有的模板示例使用的都是隐式实例化( implicit instantiation),即它们声明一个或多个对象,指出所需的类型,而编译器使用通用模板提供的处方生成具体的类定义:
ArraTP<int, 100> stuff; // implicit instantiation
编译器在需要对象之前,不会生成类的隐式实例化:
ArrayTP<double, 30> *pt; //a pointer, no object needed yet
pt = new ArrayTP<double, 30>; //now an obiect is needed
第二条语句导致编译器生成类定义,并根据该定义创建一个对象。
2.显式实例化
当使用关键字 template并指出所需类型来声明类时,编译器将生成类声明的显式实例化( explicit instantiation)。声明必须位于模板定义所在的名称空间中。例如,下面的声明将 ArrayTP< string,100>声明 为一个类
template class ArrayTP <string, 100>; //generate ArrayTP<string, 100> class
在这种情况下,虽然没有创建或提及类对象,编译器也将生成类声明(包括方法定义)。和隐式实例化样,也将根据通用模板来生成具体化。
3.显式具体化
显式具体化( explicit specialization)是特定类型(用于替换模板中的泛型)的定义。有时候,可能需要在为特殊类型实例化时,对模板进行修改,使其行为不同。在这种情况下,可以创建显式具体化。
例如,假设已经为用于表示排序后数组的类(元素在加入时被排序)定义了一个模板:
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 char *>
{
... //details omitted
}
其中的实现代码将使用 strcmp()(而不是>)来比较数组值。现在,当请求 const char*类型的 SortedArray模板时,编译器将使用上述专用的定义,而不是通用的模板定义:
SortedArray<int> scores; //use general definition
SortedArray<const char *> dates; // use specialized definition
4.部分具体化
C++还允许部分具体化( partial specialization),即部分限制模板的通用性。例如,部分具体化可以给类型参数之一指定具体的类型:
// general template
template <class T1, class T2>
class Pair
{
};
// specialization with T2 set to int
template <class T1>
class Pair<T1, int>
{
}
关键字 template后面的 <>
声明的是没有被具体化的类型参数。
因此,上述第二个声明将T2具体化为 int
,但T1保持不变。
上面的部分具体化,看着和给他设置一个默认值 int 有什么区别?
template <class T1, class T2 =int>
class Pair{
};
把他们放到一块看看效果,看下面这个代码 learn.cpp
/*
author:梦悦foundation
公众号:梦悦foundation
可以在公众号获得源码和详细的图文笔记
*/
// general template
template <class T1, class T2>
class Pair
{
};
// specialization with T2 set to int
template <class T1>
class Pair<T1, int>
{
};
//显式具体化 两个 都是int类型
template <>
class Pair<int, int>
{
};
//带默认值 和部分具体化有区别吗?
template <class T1, class T2 =int>
class Pair{
};
int main()
{
return 0;
}
程序编译运行的结果:
这里提示 他和普通模板的那个声明重定义了!编译器认为这是两个相同的声明。
book@book-desktop:~/meng-yue/c++/code_reuse/04$ g++ -o learn learn.cpp
learn.cpp:30: error: redefinition of ‘class Pair<T1, T2>’
learn.cpp:10: error: previous definition of ‘class Pair<T1, T2>’
book@book-desktop:~/meng-yue/c++/code_reuse/04$
把通普通模板注释掉去,看看效果:
general template
//template <class T1, class T2>
//class Pair
//{
//};
编译依然报错了:
这说明使用部分具体化和显式具体化的时候是依赖于 之前的普通模板的声明的。也就是说只有声明了普通的模板,编译器才知道怎么处理部分具体化的模板和显式具体化的模板
book@book-desktop:~/meng-yue/c++/code_reuse/04$ g++ -o learn learn.cpp
learn.cpp:16: error: ‘Pair’ is not a template
learn.cpp:23: error: wrong number of template arguments (2, should be 1)
learn.cpp:16: error: provided for ‘template<class T1> class Pair’
learn.cpp:30: error: redeclared with 2 template parameter(s)
learn.cpp:16: note: previous declaration ‘template<class T1> class Pair’ used 1 template parameter(s)
book@book-desktop:~/meng-yue/c++/code_reuse/04$
那我尝试把带默认值的模板放到这两个模板前面可以吗?
按照下面的排列顺序:
//带默认值 和部分具体化有区别吗?
template <class T1, class T2 =int>
class Pair{
};
// specialization with T2 set to int
template <class T1>
class Pair<T1, int>
{
};
//显式具体化 两个 都是int类型
template <>
class Pair<int, int>
{
};
再来看看编译运行的结果:
编译没有报错,说明这样是可以的。并且默认参数和部分具体化并不是同一个声明,虽然他们的功能都是一样的!
book@book-desktop:~/meng-yue/c++/code_reuse/04$ g++ -o learn learn.cpp
5. 声明了之后,怎么使用他们呢?
上面的代码,如果有下面这几种声明,会分别使用哪个模板
Pair<double, double> p1;
Pair<double, int> p2;
Pair<int, int> p3;
Pair<double> p4;
Pair<int> p5;
learn1.cpp
#include <iostream>
#include <typeinfo>
#include <string>
using namespace std;
/*
author:梦悦foundation
公众号:梦悦foundation
可以在公众号获得源码和详细的图文笔记
*/
general template
//template <class T1, class T2>
//class Pair
//{
//};
//带默认值 和部分具体化有区别吗?
template <class T1, class T2 =int>
class Pair{
public:
void Show() {
cout << "template <class T1, class T2 =int> " << ", T1=" << typeid(T1).name() << ", T2=" << typeid(T2).name() << endl;
}
};
// specialization with T2 set to int
template <class T1>
class Pair<T1, int>
{
public:
void Show() {
cout << "template <class T1> class Pair<T1, int> " << ", T1=" << typeid(T1).name() << endl;
}
};
//显式具体化 两个 都是int类型
template <>
class Pair<int, int>
{
public:
void Show() {
cout << "template <> Pair<int, int> " << endl;
}
};
int main()
{
cout << "---------------开始--->公众号:梦悦foundation---------------" << endl;
Pair<double, double> p1;
cout << "Pair<double, double> p1: ";
p1.Show();
cout << "------------------------------------------" << endl;
cout << "Pair<double, int> p2: ";
Pair<double, int> p2;
p2.Show();
cout << "------------------------------------------" << endl;
cout << "Pair<int, int> p3: ";
Pair<int, int> p3;
p3.Show();
cout << "------------------------------------------" << endl;
cout << "Pair<double> p4: ";
Pair<double> p4;
p4.Show();
cout << "------------------------------------------" << endl;
cout << "Pair<int> p5: ";
Pair<int> p5;
p5.Show();
cout << "------------------------------------------";
cout << "---------------结束--->公众号:梦悦foundation---------------" << endl;
return 0;
}
程序的运行结果:
book@book-desktop:~/meng-yue/c++/code_reuse/04$ ./learn1
---------------开始--->公众号:梦悦foundation---------------
Pair<double, double> p1: template <class T1, class T2 =int> , T1=d, T2=d
------------------------------------------
Pair<double, int> p2: template <class T1> class Pair<T1, int> , T1=d
------------------------------------------
Pair<int, int> p3: template <> Pair<int, int>
------------------------------------------
Pair<double> p4: template <class T1> class Pair<T1, int> , T1=d
------------------------------------------
Pair<int> p5: template <> Pair<int, int>
---------------------------------------------------------结束--->公众号:梦悦foundation---------------
book@book-desktop:~/meng-yue/c++/code_reuse/04$
这里需要注意的是, Pair<double> p4
它是与部分具体化匹配的,而不是通用的模板,而Pair<int> p5
是和显式具体化匹配的!
这里假设把现实具体化注释掉去,p4,p5会匹配谁呢?
learn2.cpp
#include <iostream>
#include <typeinfo>
#include <string>
using namespace std;
/*
author:梦悦foundation
公众号:梦悦foundation
可以在公众号获得源码和详细的图文笔记
*/
general template
//template <class T1, class T2>
//class Pair
//{
//};
//带默认值 和部分具体化有区别吗?
template <class T1, class T2 =int>
class Pair{
public:
void Show() {
cout << "template <class T1, class T2 =int> " << ", T1=" << typeid(T1).name() << ", T2=" << typeid(T2).name() << endl;
}
};
// specialization with T2 set to int
template <class T1>
class Pair<T1, int>
{
public:
void Show() {
cout << "template <class T1> class Pair<T1, int> " << ", T1=" << typeid(T1).name() << endl;
}
};
//显式具体化 两个 都是int类型
//template <>
//class Pair<int, int>
//{
//public:
// void Show() {
// cout << "template <> Pair<int, int> " << endl;
// }
//};
int main()
{
cout << "---------------开始--->公众号:梦悦foundation---------------" << endl;
Pair<double, double> p1;
cout << "Pair<double, double> p1: ";
p1.Show();
cout << "------------------------------------------" << endl;
cout << "Pair<double, int> p2: ";
Pair<double, int> p2;
p2.Show();
cout << "------------------------------------------" << endl;
cout << "Pair<int, int> p3: ";
Pair<int, int> p3;
p3.Show();
cout << "------------------------------------------" << endl;
cout << "Pair<double> p4: ";
Pair<double> p4;
p4.Show();
cout << "------------------------------------------" << endl;
cout << "Pair<int> p5: ";
Pair<int> p5;
p5.Show();
cout << "------------------------------------------";
cout << "---------------结束--->公众号:梦悦foundation---------------" << endl;
return 0;
}
程序运行的结果:
除了p1之外,其他的都是匹配到了部分具体化。
book@book-desktop:~/meng-yue/c++/code_reuse/04$ ./learn2
---------------开始--->公众号:梦悦foundation---------------
Pair<double, double> p1: template <class T1, class T2 =int> , T1=d, T2=d
------------------------------------------
Pair<double, int> p2: template <class T1> class Pair<T1, int> , T1=d
------------------------------------------
Pair<int, int> p3: template <class T1> class Pair<T1, int> , T1=i
------------------------------------------
Pair<double> p4: template <class T1> class Pair<T1, int> , T1=d
------------------------------------------
Pair<int> p5: template <class T1> class Pair<T1, int> , T1=i
---------------------------------------------------------结束--->公众号:梦悦foundation---------------
book@book-desktop:~/meng-yue/c++/code_reuse/04$
注意,如果指定所有的类型,则 <>
内将为空,这将导致显式具体化.
//specialization 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 specialization
Pair<int, int> p3; // use Pair<int, int> explicit specialization
Pair<double> p4; // //use Pair<T1, int> partial specialization, T1 = double
Pair<int> p5; // 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> {
};
// specialization with T3 and T2 set to T1+
template <class T1> class Trio<T1, T1*, T1*> {
};
给定上述声明,编译器将作出如下选择:
这个特性感觉很鸡肋,还必须三个参数都写上,不能只写一个或者两个,要不然会报错。
Trio<int, short, char *> t1; //use general template
Trio<int, short, short> t2; //use Trio<T1, T2, T2>
Trio<char, char * char *> t3; //use Trio<T1, T1*, T1*>
learn3.cpp
#include <iostream>
#include <typeinfo>
#include <string>
using namespace std;
/*
author:梦悦foundation
公众号:梦悦foundation
可以在公众号获得源码和详细的图文笔记
*/
// general template
template <class T1, class T2, class T3>
class Pair {
public:
void Show() {
cout << "<class T1, class T2, class T3>" << ", T1=" << typeid(T1).name() << ", T2=" << typeid(T2).name() << ", T3=" << typeid(T3).name() << endl;
}
};
// specialization with T3 set to T2
template <class T1, class T2>
class Pair<T1, T2, T2> {
public:
void Show() {
cout << "Pair<T1, T2, T2>" << ", T1=" << typeid(T1).name() << ", T2=" << typeid(T2).name() << endl;
}
};
// specialization with T3 and T2 set to T1+
template <class T1>
class Pair<T1, T1*, T1*> {
public:
void Show() {
cout << "Pair<T1, T1*, T1*>" << ", T1=" << typeid(T1).name() << endl;
}
};
int main()
{
cout << "---------------开始--->公众号:梦悦foundation---------------" << endl;
cout << "Pair<int, short, char *> t1: ";
Pair<int, short, char *> t1; //use general template
t1.Show();
cout << "------------------------------------------" << endl;
cout << "Pair<int, short, short> t2: ";
Pair<int, short, short> t2; //use Trio<T1, T2, T2>
t2.Show();
cout << "------------------------------------------" << endl;
cout << "Pair<char, char * char *> t3: ";
Pair<char, char *, char *> t3; //use Trio<T1, T1*, T1*>
t3.Show();
cout << "------------------------------------------" << endl;
cout << "---------------结束--->公众号:梦悦foundation---------------" << endl;
return 0;
}
程序运行的结果:
book@book-desktop:~/meng-yue/c++/code_reuse/04$ ./learn3
---------------开始--->公众号:梦悦foundation---------------
Pair<int, short, char *> t1: <class T1, class T2, class T3>, T1=i, T2=s, T3=Pc
------------------------------------------
Pair<int, short, short> t2: Pair<T1, T2, T2>, T1=i, T2=s
------------------------------------------
Pair<char, char * char *> t3: Pair<T1, T1*, T1*>, T1=c
------------------------------------------
---------------结束--->公众号:梦悦foundation---------------
book@book-desktop:~/meng-yue/c++/code_reuse/04$
11. 成员模板
模板可用作结构、类或模板类的成员。要完全实现STL的设计,必须使用这项特性。程序清单14.20是一个简短的模板类示例,该模板类将另一个模板类和模板函数作为其成员。
程序清单14.20 tempmemb.cpp
/*
author:梦悦foundation
公众号:梦悦foundation
可以在公众号获得源码和详细的图文笔记
*/
// tempmemb.cpp -- template members
#include <iostream>
using std::cout;
using std::endl;
template <typename T>
class beta
{
private:
template <typename V> // nested template class member
class 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 object
hold<int> n; // template object
public:
beta( T t, int i) : q(t), n(i) {
}
template<typename U> // template method
U 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";
// std::cin.get();
return 0;
}
在程序清单14.20中,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模板的外面定义它们。然而,很老的编译器根本不接受模板成员,而另一些编译器接受模板成员(如程序清单14.20所示),但不接受类外面的定义。然而,如果所用的编译器接受类外面的定义,则在beta模板之外定义模板方法的代码如下:
template <typename T>
class beta
{
private:
template <typename V> // declaration
class hold;
hold<T> q;
hold<int> n;
public:
beta(T t, int i) : q(t), n(i) {
}
template <typename U> // declaration
U 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>
类的成员,这是通过使用作用域解析运算符来完成的。
12. 将模板用作参数
您知道,模板可以包含类型参数(如 typename T)和非类型参数(如int n)。模板还可以包含本身就是模板的参数,这种参数是模板新增的特性,用于实现STL
在程序清单14.21所示的示例中,开头的代码如下:
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
{
};
在程序清单14.21中,Crab类的声明包含了了两个成员对象:
class Crab
{
private:
Thing<int> s1;
Thing<double> s2;
};
前面的legs声明将用King<int>
替换 Thing<int>
,用King< double>
替换 Thing< double>
。
程序清单14.21包含下面的声明:
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模板,因此这个例子将使用它。
程序清单14.21 tempparm.cpp
/*
author:梦悦foundation
公众号:梦悦foundation
可以在公众号获得源码和详细的图文笔记
*/
// tempparm.cpp use templates as parameters
#include <iostream>
#include "stacktp.h"
template <template <typename T> class Thing>
class Crab
{
private:
Thing<int> s1;
Thing<double> s2;
public:
Crab() {
};
// assumes the thing class has push() and pop() members
bool 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 thing
int 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";
// cin.get();
// cin.get();
return 0;
}
下面是程序清单14.,21所示程序的运行情况:
book@book-desktop:~/meng-yue/c++/code_reuse/04$ ./tempparm
Enter int double pairs, such as 4 3.5 (0 0 to end):
2 4.5
6 7.8
9 0.1
0 0
9, 0.1
6, 7.8
2, 4.5
Done.
book@book-desktop:~/meng-yue/c++/code_reuse/04$
可以混合使用模板参数和常规参数,例如,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表示非模板类型。
13. 模板类和友元
模板类声明也可以有友元。模板的友元分3类:
- 非模板友元
template <class T>
Class HasFriend
{
public:
friend void counts (); // friend to all HasFriend instantiations
}
- 约束( bound)模板友元,即友元的类型取决于类被实例化时的类型;
- 非约束( unbound)模板友元,即友元的所有具体化都是类的每一个具体化的友元。
下面分别介绍它们。
1.模板类的非模板友元函数,就是普通函数
在模板类中将一个常规函数声明为友元:
template <class T>
Class HasFriend
{
public:
friend void counts (); // friend to all HasFriend instantiations
}
上述声明使 counts()函数成为模板所有实例化的友元。例如,它将是类 HasFriend<in>
和 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> hf;
编译器将用int替代模板参数T,因此友元声明的格式如下:
class HasFriends<int>
{
friend void report(HasFriend<int> &); //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
程序清单14.22说明了上面几点。 HasFriend模板有一个静态成员ct。这意味着这个类的每一个特定的具体化都将有自己的静态成员。count()方法是所有 HasFriend具体化的友元,它报告两个特定的具体化( HasFriend<int>
和 HasFriend<double>
)的ct的值。
该程序还提供两个 report()函数,它们分别是某个特定 HasFriend具体化的友元。
程序清单14.22 frnd2tmp.cpp
/*
author:梦悦foundation
公众号:梦悦foundation
可以在公众号获得源码和详细的图文笔记
*/
// frnd2tmp.cpp -- template class with non-template friends
#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 reports(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 reports(HasFriend<int> & hf)
{
cout <<"HasFriend<int>: " << hf.item << endl;
}
// non-template friend to the HasFriend<double> class
void reports(HasFriend<double> & hf)
{
cout <<"HasFriend<double>: " << hf.item << endl;
}
int main()
{
cout << "No objects declared: ";
counts();
HasFriend<int> hfi1(10);
cout << "After hfi1 declared: ";
counts();
HasFriend<int> hfi2(20);
cout << "After hfi2 declared: ";
counts();
HasFriend<double> hfdb(10.5);
cout << "After hfdb declared: ";
counts();
reports(hfi1);
reports(hfi2);
reports(hfdb);
// std::cin.get();
return 0;
}
有些编译器将对您使用非模板友元发出警告。下面是程序清单14.,22所示程序的输出:
No objects declared: int count: 0; double count: 0
After hfi1 declared: int count: 1; double count: 0
After hfi2 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
2.模板类的约束模板友元函数, 友元函数是模板
可以修改前一个示例,使友元函数本身成为模板。具体地说,为约束模板友元作准备,要使类的每一个具体化都获得与友元匹配的具体化。这比非模板友元复杂些,包含以下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 report<> (HasFriendT<int> &);
}
基于TT 的具体化将变为int, 基于 HasFriendT<TT>
的具体化将变为 HasFriendT<int>
。因此,模板具体化 counts<int>()
和 report< HasFriendT<int> >()
被声明为 HasFriendT类的友元。
程序必须满足的第三个要求是,为友元提供模板定义。程序清单14.23 说明了这3个方面。请注意, 程序清单14.22 包含1个 count()函数,它是所有 HasFriendT类的友元;而程序清单14.23包含两个count() 函数,它们分别是某个被实例化的类类型的友元。因为count()函数调用没有可被编译器用来推断出所需具体化的函数参数,所以这些调用使用 count<int>
和 count<double>
(指明具体化。但对于 report()调用,编译器可以从参数类型推断出要使用的具体化。使用<>
格式也能获得同样的效果:
report< HasFriendT<int> > (hfi2); // same as report(hfi2);
程序清单14.23 tmp2tmp.cpp
/*
author:梦悦foundation
公众号:梦悦foundation
可以在公众号获得源码和详细的图文笔记
*/
// 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> hfi1(10);
HasFriendT<int> hfi2(20);
HasFriendT<double> hfdb(10.5);
report(hfi1); // generate report(HasFriendT<int> &)
report(hfi2); // generate report(HasFriendT<int> &)
report(hfdb); // generate report(HasFriendT<double> &)
cout << "counts<int>() output:\n";
counts<int>();
cout << "counts<double>() output:\n";
counts<double>();
// std::cin.get();
return 0;
}
下面是程序清单14.23所示程序的输出:
book@book-desktop:~/meng-yue/c++/code_reuse/04$ ./tmp2tmp
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
book@book-desktop:~/meng-yue/c++/code_reuse/04$
正如您看到的, counts<double>
和 counts<int>
报告的模板大小不同,这表明每种T类型都有自己的友元函数 count()。
3.模板类的非约束模板友元函数
前一节中的约束模板友元函数是在类外面声明的模板的具体化。int类具体化获得int函数具体化,依此类推。
通过在类内部声明模板,可以创建非约束友元函数,即每个函数具体化都是每个类具体化的友元。
对于非约束友元,友元模板类型参数与模板类类型参数是不同的:
非约束模板的友元就是 这个函数只有一个, 而且他是所有 ManyFriend 具体化类的 友元函数。 约束模板友元就是 一个具体化 就有 一个相应的 友元函数。
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;
}
程序清单14.24是一个使用非约束友元的例子。其中,函数调用show2(hfi1,hfi2)与下面的具体化匹配:
void show2< HasFriend<int> &, HasFriend<int> &>(HasFriend<int>& c, HasFriend<int> & d);
因为它是所有 HasFriend 具体化的友元,所以能够访问所有具体化的item成员,但它只访问了
ManyFriend对象。
同样,show2(hfd,hfi2)与下面具体化匹配:
void show2<HasFriend<double> & ,HasFriend<int> & > (HasFriend<double> &c, HasFriend<int> &d);
它也是所有 HasFriend具体化的友元, 并访问了 HasFriend<int>
对象的item成员和 HasFriend<double>
对象的item成员。
程序清单14.24 manyfrnd.cpp
// 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> hfi1(10);
ManyFriend<int> hfi2(20);
ManyFriend<double> hfdb(10.5);
cout << "hfi1, hfi2: ";
show2(hfi1, hfi2);
cout << "hfdb, hfi2: ";
show2(hfdb, hfi2);
// std::cin.get();
return 0;
}
程序运行的结果:
book@book-desktop:~/meng-yue/c++/code_reuse/04$ ./manyfrnd
hfi1, hfi2: 10, 20
hfdb, hfi2: 10.5, 20
book@book-desktop:~/meng-yue/c++/code_reuse/04$
14. 模板别名(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>
但如果您经常编写类似于上述 typedef的代码,您可能怀疑要么自己忘记了可简化这项任务的C++功能,要么C++没有提供这样的功能。C++11新增了一项功能一一使用模板提供一系列别名,如下所示:
template <typename 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 *(*pal)[10]; // typedef syntax
using pa2 = const int *(*)[10]; //using=syntax
习惯这种语法后,您可能发现其可读性更强,因为它让类型名和类型信息更清晰。
C++11新增的另一项模板功能是可变参数模板( variadic template),让您能够定义这样的模板类和模板函数,即可接受可变数量的参数。这个主题将在第18章介绍。