模板和泛型编程--模板编译模型--第十六章 --c++ primer(3)

当编译器看到模板定义的时候,它不立即产生代码。只有在看到用到模板时,如调用了函数模板或调用了类模板的对象的时候, 编译器才产生特定类型的模板实例。
一般而言,当调用函数的时候,编译器只需要看到函数的声明。类似地,定义类类型的对象时,类定义必须可用,但成员函数的定义不是必须存在的。因此,应该将类定义和函数声明放在头文件中, 而普通函数和类成员函数的定义放在源文件中。
模板则不同:要进行实例化,编译器必须能够访问定义模板的源代码。当调用函数模板或类模板的成员函数的时候,编译器需要函数定义,需要那些通常放在源文件中的代码。
标准 C++ 为编译模板代码定义了两种模型。在两种模型中,构造程序的方式很大程度上是相同的:类定义和函数声明放在头文件中,而函数定义和成员定义放在源文件中。两种模型的不同在于,编译器怎样使用来自源文件的定义。如本书所述,所有编译器都支持第一种模型,称为“包含”模型,只有一些编译器支持第二种模型,“分别编译”模型。

要编译使用自己的类模板和函数模板的代码,必须查阅编译器的用户指南,看看编译器怎样处理实例化。

包含编译模型
在包含编译模型中,编译器必须看到用到的所有模板的定义。一般而言,可以通过在声明函数模板或类模板的头文件中添加一条 #include 指示使定义可用, 该#include 引入了包含相关定义的源文件:


     // header file utlities.h
     #ifndef UTLITIES_H // header gaurd (Section 2.9.2, p. 69)
     #define UTLITIES_H
     template <class T> int compare(const T&, const T&);
     // other declarations

     #include "utilities.cc" // get the definitions for compare etc.
     #endif

     // implemenatation file utlities.cc
     template <class T> int compare(const T &v1, const T &v2)
     {

         if (v1 < v2) return -1;
         if (v2 < v1) return 1;
         return 0;
     }
     // other definitions

这一策略使我们能够保持头文件和实现文件的分享, 但是需要保证编译器在编译使用模板的代码时能看到两种文件。某些使用包含模型的编译器,特别是较老的编译器,可以产生多个实例。如果两个或多个单独编译的源文件使用同一模板, 这些编译器将为每个文件中的模板产生一个实例。 通常, 这种方法意味着给定模板将实例化超过一次。 在链接的时候,或者在预链接阶段,编译器会选择一个实例化而丢弃其他的。在这种情况下,如果有许多实例化同一模板的文件, 编译时性能会显著降低。 对许多应用程序而言,
这种编译时性能降低不大可能在现代计算机上成为问题, 但是, 在大系统环境中,编译时选择问题可能变得非常重要。
这种编译器通常支持某些机制,避免同一模板的多个实例化中隐含的编译进开销。编译器优化编译时性能的方法各不相同。如果使用模板的程序的编译时间难于承担,请查阅编译器的用户指南,看看你的编译器能提供什么支持以避免多余的实例化。
分别编译模型
在分别编译模型中,编译器会为我们跟踪相关的模板定义。但是,我们必须让编译器知道要记住给定的模板定义,可以使用 export 关键字来做这件事。export 关键字能够指明给定的定义可能会需要在其他文件中产生实例化。在一个程序中,一个模板只能定义为导出一次。编译器在需要产生这些实例化时计算出怎样定位模板定义。export 关键字不必在模板声明中出现。
一般我们在函数模板的定义中指明函数模板为导出的,这是通过在关键字template 之前包含 export 关键字而实现的:

     // the template definition goes in a separately-compiled source file
     export template <typename Type>
     Type sum(Type t1, Type t2)

这个函数模板的声明像通常一样应放在头文件中,声明不必指定 export。

对类模板使用 export 更复杂一些。通常,类声明必须放在头文件中,头文件中的类定义体不应该使用关键字 export,如果在头文件中使用了 export,则该头文件只能被程序中的一个源文件使用。相反,应该在类的实现文件中使用 export:

     // class template header goes in shared header file
     template <class Type> class Queue { ... };
     // Queue.ccimplementation file declares Queue as exported
     export template <class Type> class Queue;
     #include "Queue.h"
     // Queue member definitions

导出类的成员将自动声明为导出的。也可以将类模板的个别成员声明为导出的,在这种情况下,关键字 export 不在类模板本身指定,而是只在被导出的特定成员定义上指定。导出成员函数的定义不必在使用成员时可见。任意非导出成员的定义必须像在包含模型中一样对待:定义应放在定义类模板的头文件中。
Exercises Section 16.3
Exercise
16.27:
确定你的编译器使用的是哪种编译模型。编写并调用函数模板,在保存未知类型对象的 vector 中查找中间值。
(注:中间值是这样一个值,一半元素比它大,一半元素比它小。)用常规方式构造你的程序:函数定义应放在一
个文件中,它的声明放在一个头文件中,定义和使用函数模板的代码应包含该头文件。
Exercise
16.28:
如果所用的编译器支持分别编译模型,将类模板的成员函数和 static 数据成员的定义放在哪里?为什么?
Exercise
16.29:
如果你的编译器使用包含模型,将那些模板成员定义放在哪里?为什么? 警告:类模板中的名字查找
编译模板是异常困难的工作。幸好,它是由编译器作者处理的任务。不幸的是,某些复杂性被推到模板用户的身上:模板包含两种名字:
1. 独立于模板形参的那些名字
2. 依赖于模板形参的那些名字
设计者的责任是,保证所有不依赖于模板形参的名字在模板本身的作用域中定义。模板用户的责任是,保证与用来实例化模板的类型相关的所有函数、类型和操作符的声明可见。这个责任意味着,在实例化类模板的成员或函数模板的时候,用户必须保证这些声明是可见的。适当使用头文件的结构良好的程序都容易满足这两个要求。模板的作者应提供头文件,该头文件包含在类模板或在其成员定义中使用的所有名字的声明。在用特定类型定义模板或者使用该模板的成员之前,用户必须保证包含了模板类型的头文件,以及定义用作成员类型的类型的头文件。
16.4. 类模板成员
到目前为止,我们只介绍了怎样声明 Queue 类模板的接口成员,本节将介绍怎样实现该类。

标准库将 queue 实现为其他容器之上的适配器(第 9.7 节)。为了强调在使用低级数据结构中涉及的编程要点,我们将
Queue 实现为链表。实际上,在我们的实现中使用标准库容器可能是一个更好的决定。

Queue 的实现策略
如图 16.1 所示,我们的实现使用两个类:

图 16.1. Queue 的执行

  1. QueueItem 类表示 Queue 的链表中的节点,该类有两个数据成员 item和 next:
    o item 保存 Queue 中元素的值,它的类型随 Queue 的每个实例而变化。
    o next 是队列中指向下一 QueueItem 对象的指针。
    Queue 中的每个元素保存在一个 QueueItem 对象中。
  2. Queue 类将提供第 16.1.2 节描述的接口函数, Queue 类也有两个数据成员:head 和 tail,这些成员是 QueueItem 指针。
    像标准容器一样,Queue 类将复制指定给它的值。
    QueueItem 类
    首先编写 QueueItem 类:
     template <class Type> class QueueItem {
     // private class: no public section
         QueueItem(const Type &t): item(t), next(0) { }
         Type item;           // value stored in this element
         QueueItem *next;     // pointer to next element in the Queue
     };

这个类似乎已经差不多完整了:它保存由其构造函数初始化的两个数据成员。像 Queue 类一样,QueueItem 是一个类模板,该类使用模板形参指定 item成员的类型,Queue 中每个元素的值将保存在 item 中。每当实例化一个 Queue 类的时候,也将实例化 QueueItem 的相同版本。例如,如果创建 Queue<int>,则将实例化一个伙伴类 QueueItem<int>
QueueItem 类为私有类——它没有公用接口。我们这个类只是为实现Queue,并不想用于一般目的,因此,它没有公用成员。需要将 Queue 类设为QueueItem 类的友元,以便 Queue 类成员能够访问 QueueItem 的成员。第
16.4.4 节将介绍怎样做。

在类模板的作用域内部,可以用它的非限定名字引用该类。

Queue 类
现在充实 Queue 类:

     template <class Type> class Queue {
     public:
         // empty Queue
         Queue(): head(0), tail(0) { }
         // copy control to manage pointers to QueueItems in the Queue
         Queue(const Queue &Q): head(0), tail(0)
                                       { copy_elems(Q); }
         Queue& operator=(const Queue&);
         ~Queue() { destroy(); }
              // return element from head of Queue
         // unchecked operation: front on an empty Queue is undefined
         Type& front()             { return head->item; }
         const Type &front() const { return head->item; }
         void push(const Type &);       // add element to back of Queue
         void pop ();                    // remove element from head of Queue
         bool empty () const {           // true if no elements in the Queue
             return head == 0;
         }
     private:
         QueueItem<Type> *head;         // pointer to first element in Queue
         QueueItem<Type> *tail;         // pointer to last element in Queue
         // utility functions used by copy constructor, assignment, and destructor
         void destroy();                // delete all the elements

         void copy_elems(const Queue&); // copy elements from parameter
     };

除了接口成员之外,还增加了三个复制控制成员(第十三章)以及那些成员所用的相关实用函数。private 实用函数 destroy 和 copy_elems 将完成释放Queue 中的元素以及从另一 Queue 复制元素到这个 Queue 的任务。 复制控制成员用于管理数据成员 head 和 tail,head 和 tail 是指向 Queue 中首尾元素的指针,这些成员是 QueueItem<Type> 类型的值。
Queue 类实现了几个成员函数:
• 默认构造函数,将 head 和 tail 指针置 0,指明当前 Queue 为空。
• 复制构造函数,初始化 head 和 tail,并调用 copy_elems 从它的初始器复制元素。
• 几个 front 函数, 返回头元素的值。 这些函数不进行检查: 像标准 queue中的类似操作一样,用户不能在空 Queue 上运行 front 函数。
• empty 函数,返回 head 与 0 的比较结果。如果 head 为 0,Queue 为空;否则,Queue 是非空的。

模板作用域中模板类型的引用
这个类的主要部分应该是我们熟悉的。它只与我们已经定义过的类有少许区别。 新的内容是 Queue 类型和 QueueItem 类型的引用中对模板类型形参的使用(或缺少)。
通常,当使用类模板的名字的时候,必须指定模板形参。这一规则有个例外:在类本身的作用域内部,可以使用类模板的非限定名。例如,在默认构造函数和复制构造函数的声明中,名字 Queue 是 Queue<Type> 缩写表示。实质上,编译器推断,当我们引用类的名字时,引用的是同一版本。因此,复制构造函数定义其实等价于:

     Queue<Type>(const Queue<Type> &Q): head(0), tail(0)
                 { copy_elems(Q); }

编译器不会为类中使用的其他模板的模板形参进行这样的推断,因此,在声明伙伴类 QueueItem 的指针时,必须指定类型形参:

     QueueItem<Type> *head;    // pointer to first element in Queue
     QueueItem<Type> *tail;    // pointer to last element in Queue

这些声明指出,对于 Queue 类的给定实例化,head 和 tail 指向为同一模板形参实例化的 QueueItem 类型的对象,即,在 Queue 实例化的内部,
head 和 tail 的类型是 QueueItem<int>*。在 head 和 tail 成员的定义中省
略模板形参将是错误的:
QueueItem *head; // error: which version of QueueItem?
QueueItem *tail; // error: which version of QueueItem?
Exercises Section 16.4
Exercise
16.30:
如果有,指出下面类模板声明(或声明对)中哪些是非法
的。

  (a) template <class Type> class C1;
         template <class Type, int size> class C1;
     (b) template <class T, U, class V> class C2;
     (c) template <class C1, typename C2> class C3
{ };
     (d) template <typename myT, class myT> class C4
{ };
     (e) template <class Type, int *ptr> class C5;
         template <class T, int *pi> class C5;

Exercise
16.31:
下面 List 的定义不正确,怎样改正?

     template <class elemType> class ListItem;
     template <class elemType> class List {
     public:
         List<elemType>();
         List<elemType>(const List<elemType> &);
         List<elemType>& operator=(const
List<elemType> &);
         ~List();
         void insert(ListItem *ptr, elemType
value);
         ListItem *find(elemType value);
     private:
         ListItem *front;
         ListItem *end;
     };

16.4.1. 类模板成员函数
类模板成员函数的定义具有如下形式:

• 必须以关键字 template 开关,后接类的模板形参表。
• 必须指出它是哪个类的成员。
• 类名必须包含其模板形参。
从这些规则可以看到,在类外定义的 Queue 类的成员函数的开关应该是:

    template <class T> ret-type Queue<T>::member-name

destroy 函数
为了举例说明在类外定义的类模板成员函数,我们来看 destroy 函数:

     template <class Type> void Queue<Type>::destroy()
     {
         while (!empty())
             pop();
     }

这个定义可以从左至右读作:
• 用名为 Type 的类型形参定义一个函数模板;
• 它返回 void;
• 它是在类模板 Queue<Type> 的作用域中。
在作用域操作符(::)之前使用的 Queue<Type> 指定成员函数所属的类。
跟在成员函数名之后的是函数定义。在 destroy 的例子中,函数体看来很
普通的非模板函数定义,它的工作是遍历这个 Queue 的每个分支,调用 pop 除
去每一项。
pop 函数
pop 成员的作用是除去 Queue 的队头值:

  template <class Type> void Queue<Type>::pop()
     {
         // pop is unchecked: Popping off an empty Queue is undefined
         QueueItem<Type>* p = head; // keep pointer to head so we can
delete it
         head = head->next;         // head now points to next element
         delete p;                  // delete old head element

      }

pop 函数假设用户不会在空 Queue 上调用 pop。pop 的工作是除去 Queue
的头元素。 必须重置 head 指针以指向 Queue 中的下一元素, 然后删除 head 位
置的元素。唯一有技巧的部分是记得保持指向该元素的一个单独指针,以便在重
置 head 指针之后可以删除元素。
push 函数
push 成员将新项放在队列末尾:

  template <class Type> void Queue<Type>::push(const Type &val)
     {
         // allocate a new QueueItem object
         QueueItem<Type> *pt = new QueueItem<Type>(val);
         // put item onto existing queue
         if (empty())
             head = tail = pt; // the queue now has only one element
         else {
             tail->next = pt; // add new element to end of the queue
             tail = pt;
         }
     }

这个函数首先分配新的 QueueItem 对象,用传递的值初始化它。这里实际上有些令人惊讶的工作,陈述如下:
1. QueueItem 构造函数将实参复制到 QueueItem 对象的 item 成员。像标准容器所做的一样,Queue 类存储所给元素的副本。
2. 如果 item 为类类型,item 的初始化使用 item 所具有任意类型的复制构造函数。
3. QueueItem 构造函数还将 next 指针初始化为 0, 以指出该元素没有指向其他 QueueItem 对象。
因为将在 Queue 的末尾增加元素,将 next 置 0 正是我们所希望的。创建和初始化新元素之后, 必须将它链入 Queue。 如果 Queue 为空, 则 head和 tail 都应该指向这个新元素。如果 Queue 中已经有元素了,则使当前 tail元素指向这个新元素。旧的 tail 不再是最后一个元素了,这也是通过使 tail指向新构造的元素指明的。

copy_elems 函数
我们将赋值操作符的实现留作习题,剩下要编写的函数只有 copy_elems了。设计该函数的目的是供赋值操作符和复制构造函数使用,它的工作是从形参中复制元素到这个 Queue:

     template <class Type>
     void Queue<Type>::copy_elems(const Queue &orig)
     {
         // copy elements from orig into this Queue
         // loop stops when pt == 0, which happens when we reach orig.tail
         for (QueueItem<Type> *pt = orig.head; pt; pt = pt->next)
             push(pt->item); // copy the element
      }

在 for 循环中复制元素,for 循环始于将 pt 设为等于形参的 head 指针。循环进行直至获得 orig 中最后一个元素之后,pt 为 0。对于 orig 中的每个元素,将该元素值的副本 push 到这个 Queue,并推进 pt 以指向 orig 中的下一元素。
类模板成员函数的实例化
类模板的成员函数本身也是函数模板。像任何其他函数模板一样,需要使用类模板的成员函数产生该成员的实例化。与其他函数模板不同的是,在实例化类模板成员函数的进修,编译器不执行模板实参推断,相反,类模板成员函数的模板形参由调用该函数的对象的类型确定。例如,当调用 Queue<int> 类型对象
的 push 成员时,实例化的 push 函数为

     void Queue<int>::push(const int &val)

对象的模板实参能够确定成员函数模板形参,这一事实意味着,调用类模板
成员函数比调用类似函数模板更灵活。 用模板形参定义的函数形参的实参允许进
行常规转换:

  Queue<int> qi; // instantiates class Queue<int>
     short s = 42;
     int i = 42;
     // ok: s converted to int and passed to push
     qi.push(s); // instantiates Queue<int>::push(const int&)
     qi.push(i); // uses Queue<int>::push(const int&)
     f(s);       // instantiates f(const short&)

     f(i);       // instantiates f(const int&)

何时实例化类和成员
类模板的成员函数只有为程序所用才进行实例化。如果某函数从未使用,则不会实例化该成员函数。这一行为意味着,用于实例化模板的类型只需满足实际使用的操作的要求。第 9.1.1 节中只接受一个容量形参的顺序容器构造函数就是这样的例子,该构造函数使用元素类型的默认构造函数。如果有一个没有定义默认构造函数的类型,仍然可以定义容器来保存该类型,但是,不能使用只接受一个容量的构造函数。
定义模板类型的对象时,该定义导致实例化类模板。定义对象也会实例化用于初始化该对象的任一构造函数,以及该构造函数调用的任意成员:

     // instantiates Queue<int> class and Queue<int>::Queue()
     Queue<string> qs;
     qs.push("hello"); // instantiates Queue<int>::push

第一个语句实例化 Queue 类及其默认构造函数,第二个语句实例化 push
成员函数。
push 成员的实例化:

  template <class Type> void Queue<Type>::push(const Type &val)
     {
          // allocate a new QueueItem object
          QueueItem<Type> *pt = new QueueItem<Type>(val);
          // put item onto existing queue
          if (empty())
              head = tail = pt;    // the queue now has only one element
          else {
              tail->next = pt;     // add new element to end of the queue
              tail = pt;
          }
     }

将依次实例化伙伴类 QueueItem<string> 及其构造函数。
Queue 类中的 QueueItem 成员是指针。类模板的指针定义不会对类进行实
例化,只有用到这样的指针时才会对类进行实例化。因此,在创建 Queue 对象

进不会实例化 QueueItem 类,相反,在使用诸如 front、push 或 pop 这样的
Queue 成员时才实例化 QueueItem 类。
Exercises Section 16.4.1
Exercise
16.32:
为 Queue 类实现赋值操作符。
Exercise
16.33:
解释在 copy_elems 函数中新创建的 Queue 对象中的
next 指针怎样设置。
Exercise
16.34:
编写第 16.1.2 节习题中定义的 List 类的成员函数定
义。
Exercise
16.35:
编写第 14.7 节中描述的 CheckedPtr 类的泛型版本。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZenZenZ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值