• 模版与泛型编程简介


    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 模版特化

      

      

      

  • 相关阅读:
    【PA2014】【BZOJ3709】Bohater(贪心,排序)
    【POJ1328】Radar Installation(贪心,决策包容)
    【NOIP2002】【Luogu1032】字串变换
    【NOIP2010】【Luogu1199】三国游戏
    【NOIP2009】【Luogu1068】分数线划定
    【NOIP2002】【Luogu1036】选数
    【NOIP2004】【Luogu1085】不高兴的津津
    【NOIP2003】【luogu1042】乒乓球
    【NOIP2012】【Luogu1080】国王游戏(贪心,邻项交换)
    Jzoj4894 SJR的直线
  • 原文地址:https://www.cnblogs.com/hancm/p/3600879.html
Copyright © 2020-2023  润新知