《C++ primer plus》第14章:C++中的代码重用(4)

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

类模板

继承公有、私有或保护和包含并不总是能够满足重用代码的需要。例如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 class
	Item items[MAX];		// holds stack items
	int top;						// index for top stakc item
public:
	Stack();
	bool isemtpy() const;
	bool isfull() const;
	// push() returns fasle 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){
...
}

如果在类声明中定义了方法内联定义则可以省略模板前缀和类限定符。
下面的程序列出了类模板和成员函数模板。知道这些模板不是类和成员函数定义至关重要。它们是 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 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

使用模板类

仅在程序包含模板模板不能生成模板类而必须请求实例化。为此需要声明一个类型为模板类的对象方法是使用所需的具体类型替换泛型名。例如下面的代码创建两个栈一个用于存储 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 stack
    char 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-风格字符串的内置方式。答案是可以创建指针栈但如果不对程序做重大修改将无法很好地工作。编译器可以创建类但使用效果如何就因人而异了。下面解释上面的程序不太适合使用指针栈的原因然后介绍一个指针栈很有用的例子。

  1. 不正确地使用指针栈
    我们将简要地介绍 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 变量该变量总是指向相同的内存单元。确实在每当读取新字符串时内存的内容都将发生改变但每次执行压入操作时加入到栈中的地址都相同。因此对栈执行弹出操作时得到的地址总是相同的它总是指向读入的最后一个字符串。具体地说栈并没偶保存每一个新字符串因此没有任何用途。

    1. 正确使用指针栈
      使用指针栈的方法之一是让调用程序提供一个指针数组其中每个指针都指向不同的字符串。注意创建不同指针是调用程序的职责而不是栈的职责。栈的任务是管理指针而不是创建指针。
      例如假设我们要模拟下面的情况。某人将一车文件夹交付给了 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 size
    int stacksize;
    Type * items;       // holds stack items
    int 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 stack
    bool pop(Type & item);          // pop top 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 = 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> { ... };	// inheritance

template <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不要求这样做。

  1. 递归使用模板
    另一个模板多功能性的例子是可以递归使用模板。例如对于前面的数组模板定义可以这样使用它

    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
    
  2. 使用多个类型参数

    模板可以包含多个类型参数。例如假设希望类可以保存两种值则可以创建并使用 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;
    }
    
  3. 默认类型模板参数

    类模板的另一项新特性是可以为参数提供默认值

    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。模板以泛型的方式描述类而具体化是使用具体的类型生成类声明。

  1. 隐式实例化
    到目前为止本章所有的模板示例使用的都是隐式实例化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
    

    第二条语句导致编译器生成类定义并根据该定义创建一个对象。

  2. 显式实例化
    当使用关键字 template 并指出所需类型来声明类时编译器将生成类声明的显式实例化explicit instantiation。声明必须位于模板定义所在的名称空间中。例如下面的声明将 ArrayTP<string, 100> 声明为一个类

    template class ArrayTP<string, 100>;	// generate ArrayTP<string, 100> class
    

    在这种情况下虽然没有创建或提及类对象编译器也将生成类声明包括方法定义。和隐式实例化一样也将根据通用模板来生成具体化。

  3. 显式具体化
    显式具体化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
    
  4. 部分具体化
    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 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";

    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>	// 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> 类的成员这是通过使用作用域解析运算符来完成的。

将模板用作参数

您知道模板可以包含类型参数如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() 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";

    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模板友元即友元的所有具体化都是类的每一个具体化的友元。

下面分别介绍它们。

  1. 模板类的非模板友元函数
    在模板类中将一个常规函数声明为友元

    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
    
  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 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()。

  3. 模板类的非约束模板友元函数
    前一节中的约束模板友元函数是在类外面声明的模板的具体化。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 章介绍。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: c++

“《C++ primer plus》第14章:C++中的代码重用(4)” 的相关文章