模版与泛型编程简介

1 函数模版 

  函数模版:独立于类型的函数,可作为一种方式产生函数特定类型版本。

  格式:template <typename T1,  …>  + 正常的函数声明三要素。<>中的为模形参表,使用逗号分割。

       注:模版形参表不能为空,为空为模版特化形式。

  示例:

  template <typename T>

  int compare(const T &val1, const  T &val2)

  {

    if (v1 < v2)

      return -1;

    if (v2 < v1)

      reutn 1;

    return 0;

  } 

1.1 函数模版使用

  使用函数模版时,编译器会推断模版实参的类型(类模版不会推断),确定了函数模版就实例化了函数模版的一个实例。即编译器承担了为我们使用的每种类型编写函数的工作。

  compare(3, 4);               //编译器实例化了 int compare(const int&, const int&);用int代替T

  compare(string("hancm"), string("hi"));  //编译器实例化了 int compare(const string&, const string&);用string代替T

1.2 inline 函数模版

  inline关键字放在template之后,例如

  //正确

  template<typename T> inline T min(const T&, const T&);

  //错误

  inline template<typename T> T min(const T&, const T&);

 

2 类模版

  同样以关键字template开头,后接模版形参表,类模版的定义与其它类相似。

  

  类模版的使用:STL中的容器都是使用类模版定义的,可以作为使用参考。

  下面以自定义一个Queue类作为实例:

  template <typename Type>

  class Queue {

  public:

    //default constructor

    Queue();

    

    //operation

    void push(const Type&);

    void pop();

    Type& front();

    const Type&  front() const;

    

  private:

    //...

  };

  

  Queue<int> qi;    //Type 被替换为int型,类模版必须指定现实模版参数。

 

3 模版形参表

  模版形参:类型形参,跟在class或typename后面,代表一个未知类型。class与typename没有区别,只是typename是标准C++组成部分。

         非类型形参,跟在类型说明符之后,代表一个未知的常量表达式。

   注:模版形参的名称没有任何不同,就如同函数形参一样。不同之处在于函数形参类型取决于类型说明符,而模版形参取决于是类型形参还是非类型形参。

  模版形参作用域

    模版形参的名字:在声明为模版形参之后到模版声明或定义的末尾。遵循常规名字屏蔽规则。

  使用模版形参名字的限制:模版形参的名字不能在模版内部重用。同时意味着同一模版形参表中名字只能使用一次。

    //错误

    template <typename T>

    class example {

      typedef double T;    //不允许
    };

    template <typename T, typename T>...   //错误

  模版声明

    模版可以只声明而不定义,但是必须指出函数或类是模版。

    //ok: 声明而不定义

    template <typename T> int compare(const T&, const T&);

    同一个模版声明和定义中,模版形参名字可以不相同。与名字无关只取决于类型: 类型形参 or 非类型形参。

    模版中typename或class不能省略:

      // error: 必须有class or typename

      template <typename T, U>...

 3.1模版的类型形参

  类型形参:由关键字class或typenaem后接说明符构成,模版形参表中两个关键字含义相同,指出后面接的名字表示未知类型。

         可以作为类型说明符在模版的任何位置,与内置类型说明符或类类型说明符使用方式相同。

  注:typename可以在模版内部指定类型,例如

    template <class parm, class T>

    parm fcn(parm *arr, T val)

    {

      typename parm::size_type *p;    //指出p为指向parm内部的size_type的指针。

                          //若没有typename, 编译器默认解释size_type为parm中static类型变量,这就变成了一个乘法表达式。
    }

    提示:在类型前指定typename是一个好方式。

3.2 模版的非类型形参

  非类型形参:用值代替,值的类型在模版形参表中指定,例如:

    //T (&arr)[N]中arr为数组的应用,N为数组的长度,是模版内部的常量。

    template <class T, size_t N> void arr_init(T (&arr)[N])   

    {

      for (int i = 0; i  != N; ++i) {

        arr[i] = i;
      }
    }

     int x[3];

    arr_init(x);    //arr类型为int[3], T = int, N = 3; 数组传递引用时,检测数组长度。

 

 4 编写泛型函数

  编写模版代码时,对实参类型的要求尽可能少是有益的。

  重要原则:模版形参是引用形参。

       函数体测试只用<比较。减少类型依赖,使模版中的一组有效表达式要求降低。 

 

5 实例化

  模版本身不是类或函数。编译器用模版产生类或函数的特定类型版本。

  实例化:产生模版的特定类型实例的过程。

  模版在使用时进行实例化:

  类模版:引用实际模版类类型时

  函数模版:调用函数模版时

                  对函数指针进行初始化或赋值时

  1.类的实例化

  当编写Queue<int> iq;时,编译器创建Queue<int>的类。编译器通过用int代替模版形参的每次出现重新编写Queue模版而创建Queue<int>类。

  类模版每次实例化都产生一个独立的类型,各独立化的类型之间没有任何关系,相互之间也没有特殊的访问权。

  注:类模版形参是必须的,不能省略。例如:Queue不是类型,而Queue<int>是类类型。

  2. 函数模版实例化

  使用函数模版时,编译器一般会推断模版实参。例如:

  compare(3.2, 3.4);  //编译器推断模版实参为double型

5.1 函数模版实参推断

  模版实参推断:从函数实参确定模版实参的类型和值的过程。

  1. 多个类型形参的实参必须完全匹配

  template <typename T> int compare(const T&, const T&);

  compare(short, int);是错误的,实参类型不匹配。推断出的模版实参必须同一个类型(能进行转换也不行)。想要这个调用成立,必须定义两个模版形参:

  template <typename T, typename U> int complate(const T&, const U&);

  2. 类型形参的实参的受限转换

  一般,不会转换实参以匹配已有的实例化,相反产生新的实例。

  编译器会执行两种转换:

  (1) const转换:接受const引用或const指针的函数,可以分别用非const对象的应用或指针来调用,不产生新实例。

            例如:

            template <typename T> T fun(const T&, const T&);

            const string s1("hancm");

            string s2("hi");

            fun(s1, s2);  //ok: s2的非const对象转换为const的引用

            接受非引用类型的函数,形参类型和实参都忽略const,即无论传递const或非const对象给接受非引用类型的函数,都使用相同的实例化。

            例如:

            template <typename T> T fun(T, T);

            fun(s1, s2);  // ok: s1为const string, s2为非const,调用fun(string, string); const被忽略,传值方式,传递一个副本。

  (2) 数组或函数到指针的转换:若模版形参不是引用类型,对数组或函数类型的实参应用常规指针转换。数组实参转换为指向第一个元素的指针,函数实参被当作指向函数类                                                            型的指针。

                  例如:

                  template <typename T> T fun(T, T);

                  int a[4], b[3];

                  fun(a, b);    //ok: calls fun(int*, int*);

                  template <typename T> T fun&(const T&, const T&);

                  fun&(a, b);    //error: 数组类型不匹配,实参不转换为指针,数组引用检测长度,长度作为参数类型的一部分。

  3. 非模版实参的常规转换

  类型转换的限制只适用于类型为模版形参的那些实参。

  普通类型定义的形参可以使用常规转换。

  template <class Type> Type sum(const Type&, int op2);  

  sum(189, double);      //ok: double转换为int,instantiates sum(int, int);

  sum(33, string("hancm"));  //error: string 不能转换为int

  4. 模版实参推断与函数指针

  使用函数模版初始化或赋值函数指针,编译器使用指针的类型实例化具有适当模版实参的模版版本。

  template <typename T> int compare(const T&, constructionT&);

  //pf指向实例化的 int compare(cosnt int&, cosnt int&);

  int (*pf) (const int&, const int&) = compare;

  若不能从函数指针类型确定模版实参,就会出错。

  void fun(int (*) (const string&, const string&));

  void fun(int (*) (const int&, cosnt int&));

  fun(compare);    //error: 不知道实例化哪个版本

5.2 函数模版的显示实参

  当不能推断模版实参时,必须覆盖模版实参推断机制,显示指定模版形参的类型或值。

  最常出现:函数返回类型与形参表中所用的所有类型都不同时。

  1. 指定显示模版实参

  考虑:

  template <class T, class U> ??? sum(T, U);

  sum(3, 4L);  //4L更大,want U sum(T, U);

  sum(3L, 4);  //3L更大,want T sum(T, U);

  解决办法:

  sum(static_cast<int>short, int);  //返回类型都一样

  2. 在返回类型中使用类型形参

  另一个解决方法:引入第三个模版形参,由调用者显示指定

  template <class T1, class T2, class T3>

  T1 sum(T2, T3);

  问题:没有实参的类型用于推断T1类型

  解决方法:调用者显示提供实参,类似类模版使用

  //ok: T1 explicitly specified: T2, T3 inferred from argument types

  long val = sum<long>(int, long);

  显示模版实参与模版形参表从左到右相对应。<long>对应T1。

  3. 显示实参与函数模版指针

  void fun(int (*) (const string&, const string&));

  void fun(int (*) (const int&, cosnt int&));

  fun(compare<int>);    //ok: 显示指定int版本

 

6 模版编译模型

  编译器看到模版定义时,不立即产生代码。只有当用到模版时,如调用了函数模版或定义了类模版的对象的时候,编译器才产生特定类型的模版实例。

  调用函数:编译器要看到函数声明。

  定义类类型对象时:类定义必须可见,成员函数的定义不是必须存在。

  结果:类定义和函数声明放在头文件中,普通函数和类成员函数定义在源文件中。这是分别编译模型。

 

  模版不同:要进行实例化编译必须能够访问定义模版的源文件。当调用函数模版或类模版的成员函数时,编译器需要函数定义,需要那些通常放在源文件中的代码。

       即,模版的相关定义也放在头文件中。这是包含编译模型,所有编译器都支持。

  1. 包含编译模型

  编译器必须看见所有模版的定义:

 

  //header file Queue.h

  #ifndef __QUEUE_H__

  #define __QUEUE_H__

  

  

  template <class T>

  class Queue {

    ...
  };

  #include "Queue.cc"

  #endif

   

  //implemenation file Queue.cc

  //成员函数定义,静态成员的定义        

  问题:某些包含编译模型的编译器(特别是较老的编译器),可以产生多个实例。如果多个单独编译的源文件使用同一模版,这些编译器将为每个文件中的模版产生一个实例。通常这意味着给定模版实例化超过一次。链接时或预链接阶段,编译器会选择一个实例而丢弃其它的,如果有许多实例化同一模版的文件,编译是性能会显著下降。

  解决方法:看看编译器提供什么支持以避免多余的实例化,避免同一模版的多个实例化中隐含的编译时开销。

  2. 分别编译模型

  编译器为我们跟踪相关的模版定义。使用关键字export使编译器记住给定的模版定义。

  export关键字指明给定的定义可能需要在其他文件中产生实例化。一个程序中,一个模版只能定义为导出一次。export不必在模版声明中出现。

  函数模版:在函数模版的定义中template之前包含export关键字,指明函数模版为导出的。

         // 在分别编译源文件的函数模版定义中

         export template <typename Type>

       Type sum(Type t1, Type t2);

         函数模版的声明放在头文件中,不必声明为export。

  类模版:类模版声明放在头文件中,头文件的类定义体不应该使用关键字export,若用了,该头文件只能被程序的一个源文件使用。

      应该在类的实现文件中使用export

      //类模版头文件Queue.h

      template <typename T> class Queue {};

 

      //类模版实现文件Queue.cc  

         export template <class Type> Queue;

      #include "Queue.h"

      //Queue成员函数定义

      导出类模版的成员自动生明为导出的。类模版的个别成员可以声明为导出的,此时,export不再类模版本身指定,在要导出的特定成员定义上指定。导出成员函数的定    

        义不必在使用成员时可见。所有非导出成员的定义必须定义在头文件中。

 

7 类模版成员

  下面以Queue类模版为例介绍类模版成员。

  注:本Queue以低级数据结构链表实现,标准库默认以deque实现。

  1. 模版作用域内引用模版类型

  在类模版的作用域内部,可以用类模版的非限定名。例如:

  Queue (const Queue<Type> &q);  //Queue的复制构造函数

  编译器推断:引用类的名字时,引用的是同一个版本。

  Queue的复制构造函数等价于:

  Queue<Type> (const Queue<Type> &q);

  注:编译器不会为类中使用的其它模版的模版形参进行这样的推断。例如:在伙伴类Queueitem中,必须指定形参类型。

  Queueitem<Type> *head;

  Queueitem<Type> *tail;

  <Type>不能去掉。

  2. 类模版成员函数

  形式如下:  

  (1) 以关键字template开头,后接类的模版形参表。

  (2) 必须指定是哪个类的成员。

  (3) 类名必须包含去模版形参。

  template <class T> return_val Queue<T>::member_function_name;

  

  3. 类模版成员函数的实例化

  类模版成员函数本身是函数模版,需要使用类模版的成员函数产生成员的实例化。实例化类模版成员函数时,编译器不进行模版实参推断,模版形参由调用该函数的对象确定。

  注:模版形参定义的函数形参的实参允许常规类型转换。

  4. 何时实例化类和成员

  类模版的成员函数: 为程序所用时进行实例化。若成员函数从未使用,则不进行实例化。

  定义模版类型对象: 实例化类模版。实例化用于初始化该对象的任一构造函数(此时构造函数被调用),以及构造函数调用的任意成员。

  例如:

  Queue<string> sq;      //实例化类模版,实例化默认构造函数。

  sq.push_back("hancm");     //实例化成员函数push_back。

 

7.1 非类型形参的模版实参

  考虑标准库中的bitset,使用的就是非类型形参。

  template <int n> class bitset { };  

  bitset<10>定义一个10为的bitset。

  注:非类型模版形参: 编译时常量表达式。

 

7.2 类模版的友元声明

  三种友元声明:

  (1)1->n: 普通非模版类型或函数的友元声明,将友元关系授予明确指定的类或函数。

    例如:

    template <class Type>

    class bar {

      //授予对普通的非模版类或函数的访问权

      friend class foobar;

      freind void fun();
    };

  (2)n->n: 类模版或函数模版的友元声明,授予对友元所有实例的访问权。

    例如:

    template <calss Type>

    class bar {

      //授予任意类模版或函数模版访问权,使用<class T>指明模版形参可以和<class Type>不同。

      template <class T> friend class foobar;

      template <class T> friend void templ_fun(const T&);
    };

  (3)1->1: 只授予对类模版或函数模版的特定实例的访问权的友元声明。

    例如:

    template <class T> class foo;

    template <class T> void temp_fun(const T&);

    template <calss Type>

    class bar {

      //授予特定的实例访问权

      //前面必须有模版的声明,否则编译器会认为该友元是一个普通非模版或非模版函数。

      friend class foo<char*>;

      friend void temp_fun<char*>(char* const&);

      更常见的: 声明相同实参的友元。只授予相同类型的模版访问权。

      friend class foo<Type>;

      friend void temp_fun<Type>(const Type&);
    };

7.3 成员模版

  成员模版:类(模版或非模版)的成员,该成员为类模版或函数模版。

  例如:

  template <class Type>

  class Queue {

  public:

    //成员模版:本身就是一个模版,只不过是一个类的成员,它的形参类型与所属类的形参类型无关。

    template <class Iter> void assign(Iter, Iter);
  };

  在类外定义成员模版:

  template <class T>  //所属类的模版形参

  template <class Iter>  //成员模版本身的模版形参

  void Queue<T>::assign(Iter beg, Iter end)

  {

  }

   注:成员模版遵循常规访问控制

    实例化:类模版形参由调用函数的对象类型确定

        成员模版的模版形参由模版实参推断出。

   

 7.4 类模版的static成员

  例如:

  template <class T>

  class foo {

  public:

    static std::size_t count() { return ctr; }

  private:

    static std::size_t ctr;
  };

  1.使用

  //ok

  foo<int>::ctr;

  foo<int>::count();

  //error

  foo::ctr;  //foo是类模版不是类,只有foo<int>这种类才可以

  2.定义static成员

  template <class T>

  size_t foo<T>::ctr = 0;    //与普通类的static类似

 

8 Queue的完整实现  

//Queue.h头文件

  

#ifndef __Queue_H__
#define __Queue_H__

#include <iostream>

//类模版声明,定义在Queue.cc文件夹中
template <typename Type> class Queue;

//重载输出操作符,不能为类的成员函数
template <typename Type> 
std::ostream& operator<<(std::ostream&, const Queue<Type>&);

//伙伴类模版,用于实现底层数据结构,标准库使用deque实现
template <typename Type>
class Queueitem {
    //需要访问Queueitem的构造函数,next指针
    friend class Queue<Type>;
    //需要访问item和next
    friend std::ostream& 
    operator<< <Type>(std::ostream&, const Queue<Type>&);

    //private section
    Queueitem(const Type &t): item(t), next(0) { };
    Type item;
    Queueitem *next;
};


template <class Type>
class Queue {
    //Needs access to head
    //需要访问Queue的head
  //使用与类模版相同类型的实参,前面必须有operator<<的声明   //否则编译器会认为这是一个非模版类型
friend std::ostream& operator<< <Type> (std::ostream&, const Queue<Type>&); public: //默认构造函数 Queue(): head(0), tail(0) { } //使用一对迭代器的构造函数,属于成员模版 template <class It> Queue(It beg, It end): head(0), tail(0) { copy_elems(beg, end); } //复制构造函数 Queue(const Queue &q): head(0), tail(0) { copy_elems(q); } //赋值操作符 Queue& operator=(const Queue&); //析构函数 ~Queue() { destroy(); }; //使用一对迭代器的赋值成员模版,标准queue没有这个函数 template <class Iter> void assign(Iter, Iter); //对列尾部添加一个元素 void push(const Type&); //从对头删除元素 void pop(); //取对头元素 Type &front() { return head->item; }; const Type &front() const { return head->item; }; //队列是否空 bool empty() const { return head == 0;}; private: Queueitem<Type> *head; Queueitem<Type> *tail; //utility functions used by copy constructor, assignment, and destructor //供析构函数调用 void destroy(); //供复制构造和复制操作符调用 void copy_elems(const Queue&); //供template <class It> Queue(It beg, It end)调用 template <class Iter> void copy_elems(Iter, Iter); }; //Include Compilation Model: include member function definitions as we;; //包含编译,大多数编译器不支持export分别编译 #include "Queue.cc" #endif

 

//Queue.cc实现文件

  

/*编写要考虑以下内容
* 1.一般先编写基本功能函数,
*    基本功能函数有增加,删除,访问, 是否空,
*    构造函数和赋值操作符要调用复制元素的函数,析构函数要调用的析构元素的函数
*    对应Queue中的push(), pop(), front()(inline), empty()(inline), copy_elems(), destroy()
*   其他函数调用它们
* 2.注意哪些函数可以是inline的,可以的尽量满足, 并且放在头文件中。
*     Queue中inline有front(), empty(),构造函数和复制构造函数,析构函数
* 3.编写函数时从需求条件最少的开始,
*  以功能型函数形式进行编写,例如push()只需要inline的empty(),pop()都不需要可以先编写
*/

#include <iostream>
 
using std::ostream;
  
template <typename Type>
void Queue<Type>::push(const Type &val)
{
    Queueitem<Type> *p = new Queueitem<Type>(val);
     
    if (empty()) {
        head = tail = p;
     } else {
         tail->next = p;
         tail = p;
     }
}
 
template <typename Type>
void Queue<Type>::pop()
{
    Queueitem<Type> *p = head;
    head = head->next;
    delete p;
}
 
template <typename Type>
void Queue<Type>::destroy()
{
    while (!empty()) {
        pop();
    }
}
 
template <typename Type>
void Queue<Type>::copy_elems(const Queue &orig)
{
    for (Queueitem<Type> *p = orig.head; p; p = p->next) {
        push(p->item);
    }
}
 
template <typename Type>
Queue<Type>& Queue<Type>::operator=(const Queue &rhs)
{
    if (this != &rhs) {
        destroy();
        copy_elems(rhs);
    }

    return *this;
}

/*
* 注意成员模版的编写方式     
* 好处:可以应用隐式类型转换,
* 即只要*Iter可以转换为Type,就可以使用assign,不需要*Iter一定和Type相同
*/
template <typename Type>    //类模版的模型形参
template <typename Iter>    //成员模版的模版形参
void Queue<Type>::assign(Iter beg, Iter end)
{
    destroy();
    while (beg != end) {
        push(*beg);
        ++beg;
    }
}

template <typename Type>
ostream& operator<<(ostream &os, const Queue<Type> &q)
{
    os << "< ";
    Queueitem<Type> *p;
     for (p = q.head; p; p = p->next) {
         os << p->item << " ";
     }
     os << ">";
}

 

//Queue_main.cc : 使用Queue的主函数

 1 #include "Queue.h"
 2 #include <iostream>
 3 #include <vector>
 4 
 5 using std::vector;
 6 using std::cout;
 7 using std::endl;
 8 
 9 int main(void) 
10 {
11     Queue<int> iq;
12     
13     cout << "在Queue中添加0..9十个元素:" << endl;
14     for (int i = 0; i != 10; ++i) {
15         iq.push(i);
16     }
17     cout << "实例化oprator<<() 输出元素:" << iq << endl << endl;
18 
19     cout << "实例化front()(用于输出元素), pop(), empty().pop后iq变为空:" << endl;
20     for (int i = 0; i != 10; ++i) {
21         cout << iq.front() << " ";
22         iq.pop();
23     }
24     cout << endl << "iq 是否为空" << endl;
25     cout << "iq.empty() == " << iq.empty();
26     cout << endl << endl;
27 
28     cout << "重新初始化iq为0..4:" << endl;
29     for (int i = 0; i != 5; ++i) {
30         iq.push(i);
31     }
32     cout << "输出iq:" << iq << endl << endl;
33 
34     Queue<int> iq2;
35     vector<int> ivec;
36     cout << "初始化vector 0..12:" << endl;
37     for (int i = 0; i != 13; ++i) {
38         ivec.push_back(i);
39         cout << ivec[i] << " ";
40     }
41     cout << endl << "实例化assign(), 用vector初始化Queue:" << endl;
42     iq2.assign(ivec.begin(), ivec.end());
43     cout << "After assign(vector); iq2:"
44          << iq2 << endl << endl;
45 
46     Queue<int> iq3 = iq2;
47     cout << "实例化复制构造函数Queue(const Queue&), Queue<int> iq3 = iq2; iq3: " << endl;
48     cout << iq3 << endl << endl;
49 
50     iq = iq2;
51     cout << "实例化复制操作符operator=(const Queue&),After iq = iq2:" << endl; 
52     cout << iq << endl << endl;
53 
54     return 0;
55 }

 使用G++编译:

  g++ Queue_main.cc    //使用GCC编译

  ./a.out            //运行

 输出:

 

注:辅助函数可以改写为list,deque,基本框架不变。

 

9 模版特化

  

 

  

 

  

 

 

 

 

 

 

 

 

 

 

posted on 2014-04-02 16:38  hancmhi  阅读(259)  评论(0编辑  收藏  举报