• c++--模板与泛型编程


    专题--泛型编程的基础(模板)

      面向对象编程(OOP)和泛型编程都能处理在编写程序时不知道类型的情况。不同之处在于:OOP能处理类型在程序运行之前都未知的情况(动态绑定);而泛型编程中,在编译时就能获知类型了。模板是泛型编程的基础。

    一、 函数模板

    1. 适用情况:如果两个函数几乎是相同的,唯一的差异是参数的类型,函数体则完全一样。

    2. 定义

      template <模板参数列表(以逗号分隔)>

    1 template <typename T>
    2 int compare(const T &v1,const T &v2)
    3 {
    4     if (v1<v2) return -1;     //假设类型T支持<操作
    5     if (v2<v1) return 1;
    6 }

    3. 实例化函数模板  

      当调用一个函数模板时,编译器用函数实参推断模板实参。

    4. 模板参数类型

      类型参数T的用途:指定返回类型,指定函数参数类型,在函数体内用于变量声明,变量类型转换

    5. 非类型模板参数

      一个非类型参数表示一个值而非一个类型。当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替,这些值必须是常量表达式

      用途:在模板定义内,模板非类型参数是一个常量值。在需要常量表达式的地方,可以使用非类型参数,例如,指定数组大小。

    //编写一个conpare版本处理字符串字面常量。
    //由于不能拷贝一个数组,所以我们将参数定义为数组的引用
    template <unsigned N,unsigned M>
    int compare(const char (&p1)[N],const char (&p2)[M])
    {
        return strcmp(p1,p2);
    }
    
    //调用compare
    compare("hi","mom");
    //编译器在字符串字面常量末尾插入一个空字符作为终结符,因此编译器实例化出如下版本
    int compare(const char (&p1)[3],const char (&p2)[4])

    6. inline和constexpr的函数模板  

      template <typename T> inline T min(const T&,const T&)        //注意inline位置

    7. 编码原则

      compare函数编写泛型代码的原则:模板中的参数是const 的引用;函数体中的条件判断仅使用<比较运算;

      一个原则:模板程序应该尽量减少对实参类型的要求。

    8. 模板编译

    注意:

      1)只有当我们实例化出模板的一个特定版本时,编译器才会生成代码;

      2)函数模板和类模板成员函数的定义通常放在头文件中;

      3)大多数编译错误发生在实例化期间报告。

      保证传递给模板的实参支持模板所要求的的操作,以及这些操作能正确工作,是调用者的责任。

    二、类模板

      与函数模板不同之处是,编译器不能为类模板推断参数类型。为了使用类模板,必须在模板名后的< >中提供额外信息(显式模板实参)。

    1. 用途:与类不同,模板可以用于更多类型的元素。

    2. 定义

    template <typename T>
    class Blob
    {
    public:
        typedef T value_type;
        typedef typename std::vector<T>::size_type size_type;
        Blob();
        T& back();
        T& operator[](size_type i);
    private:
        std::shared_ptr<std::vector<T>> data;
        void check(size_type i,const std::string &msg) const;
    }

     3. 实例化类模板

      与标准库容器(STL)相同,使用Blob时,用户需要指出元素类型。也就是需要提供显式模板实参

      Blob<int> ia;

      Blob<int> ia2={0,1,2,3,4};

      Blob<string> names;

      Blob<double> price;

      一个类模板的每个实例都形成一个独立的类,类型Blob<string>与任何其他Blob类型都没有关联,也不会对任何其他Blob类型的成员有特殊访问权限。

    4. 在类模板作用域中引用模板类型

      类模板用来实例化类型,而一个实例化的类型总是包含模板参数的。无论何时使用模板都必须显式提供模板实参。但这一规则有一个例外,在类模板自己的作用域中,我们可以直接使用模板名而不提供实参。

    5. 类模板的成员函数

      注意:定义在类模板之外的成员函数,必须以关键字template开始,后接类模板参数列表:

    1 //template <typename T>
    2 //ret-type Blob<T>::member-nam(parm-list)
    3 
    4 template <typename T>
    5 void Blob<T>:;check(size_type i,const std::string &msg) const
    6 {
    7     if  (i>=data->size())
    8         throw std::out_of_range(msg)
    9 }

    6. 类模板成员函数的实例化  

      成员函数只有在被用到时才进行实例化,这一特性使得即使某种类型不能完全符合模板操作的要求,我们仍然能用该类型实例化类。但是,实例化定义template declaration)则不同(C++ Primer5 P598),实例化定义会实例化所有成员。

      当模板被使用时才会进行实例化,这一特性意味着,相同的实例可能出现在多个对象文件中。在大系统中,在多个文件中实例化相同模板的额外开销可能非常严重。在新标准中,我们可以通过显式实例化来避免这种开销。一个显式实例化有如下形式:(分别放在不同的两个.cpp文件)

      extern template declaration;                                 //实例化声明

      template declaration;                                            //显式实例化定义

    declaration是一个类或函数声明,其中所有模板参数已被替换为模板实参。例如:

      extern template class Blob<string>                     //声明

      template int compare(const int&,const int&);    //定义

      当编译器遇到extern模板声明时,它不会在本文件中生成实例化代码。将一个实例化声明为extern就表示承诺程序在其他位置有该实例化的一个非extern定义。对于一个给定的实例化版本,可能有多个extern声明,但必须只有一个定义。

    7. 类模板和友元

    (C++ Primer P588)

     1 //func.h
     2 #ifndef __FUNC_H__
     3 #define __FUNC_H__
     4 
     5 /*对于一个模板类C*/
     6 template <typename> class Pal;  //类模板Pal的前置声明
     7 template <typename T>
     8 class C
     9 {
    10     //C的每个实例将相同实例化的Pal声明为友元.类模板Pal必须有前置声明
    11     friend class Pal<T>;                     //相同实例(T)
    12     //类模板Pal2的所有实例都是C的每个实例的友元。不需要前置声明,友元声明中必须使用与类模板本身不同的模板参数X
    13     template <typename X> friend class Pal2; //所有实例
    14     //Pal3是普通类(非模板类),它是C所有实例的友元
    15     friend class Pal3;
    16 };
    17 
    18 /***************************************************/
    19 
    20 /*对于一个普通类(非模板类)C2*/
    21 template <typename T> class PP;   //前置声明
    22 class C2
    23 {
    24     //用类C2实例化的Pal是C2的一个友元。需要前置声明
    25     friend class PP<C2>;                     //特定实例
    26     //PP2的所有实例都是C2的友元。无需前置声明
    27     template <typename T> friend class PP2;  //所有实例
    28 };
    29 
    30 #endif // __FUNC_H__
    31 
    32 //main.cpp
    33 #include <iostream>
    34 #include "func.h"         //
    35 using namespace std;
    36 
    37 int main()
    38 {
    39     return 0;
    40 }

    总结:

      1. "特定实例"或"相同实例"作为友元时,必须前置声明。

      2. "所有实例"成为友元时,不需要前置声明。

      3. 为了让所有实例成为友元,友元声明中必须使用与类模板本身不同的模板参数(如 X)。

    8. 模板类型别名(感觉用处不大)

      1) typdef引用实例化的类: typedef Blob<string> StrBlob;

      2) 为类模板定义一个类型别名

        template <typename T> using twin=pair<T,T>;            //一组类的别名

        twin<double> authors;                                                   //pair<double,double> authors;

        twin<int> win_loss;                                                        //pair<int,int> win_loss;

      3) 定义一个模板类型别名时,可以固定一个或多个模板参数:

        template <typename T> using partNo=pair<T,unsigned>  //固定一个参数unsigned

        partNo<string> books;                                   //pair<string> books;

        partNo<Student> kids;                                   //pair<Student> kids;

    9. 类模板的static成员

      模板类的每个static成员必须有且仅有一个定义。但是,类模板的每个实例都有一个独有的static对象(也就是相同类型对象共用同一个static成员)。类似任何其他成员函数,一个static成员函数只有在使用时才会被实例化。

     1 template <typename T>
     2 class Foo
     3 {
     4 public:
     5     static std::size_t count() {return str;}
     6 private:
     7     static std::size_t ctr;
     8 }
     9 
    10 //实例化static成员Foo<string>::ctr和Foo<string>::count
    11 Foo<string> fs;
    12 //所有3个对象共享相同的Foo<int>::ctr和Foo<int>::count成员
    13 Foo<int> fi,fi2,fi3;

     三、模板参数

    1. 两条原则:

      1) 模板参数会隐藏外层作用域中声明的相同名字;

      2) 与大多数其他上下文不同,在模板内不能重用模板参数名;

    1 typedef double A;
    2 template <typename A,typename B>
    3 void f(A a,B b)
    4 {
    5     A tmp=a;      //tmp的类型为模板参数A的类型
    6     double B;      //错误:重声明模板参数B
    7 }

    2. 使用类的类型成员

      默认情况下,C++语言假定通过作用域运算符访问的名字不是类型。因此,如果希望使用一个模板类型参数的类型成员,就必须显式告诉编译器改名字是一个类型。只能通过关键字typename来实现这一点。

     1 template <typename T>
     2 typename T::value_type top(const T& c)
     3 {
     4     if (!c.empty())
     5         return c.back();
     6     else
     7         return typename T::value_type();
     8 }
     9 
    10 
    11 //区别:
    12 //T::value_type                 访问静态成员
    13 //typename T::value_type  访问类型成员

     四、成员模板

      成员模板不能是虚函数。

    1. 普通类(非模板)的成员模板

    2. 类模板的成员模板

      在类模板外定义一个成员模板时:必须同时为类模板和成员模板提供模板参数列表。类模板的参数列表在前,后跟成员自己的模板参数列表:

    template <typename T>         //类的类型参数
    template <typename It>        //构造函数的类型参数
    Blob<T>::Blob(It b,It e):data(std::make_shared<std::vetor<T>>(b,e)){}

     五、模板实参推断

      从函数实参来确定模板实参的过程被称为模板实参推断。

    1. 类型转换与模板类型参数


      C++中类型转换有以下几种:

       1) const转换:可以将一个非const对象的引用(或指针)传递给一个const的引用(或指针)形参。

      2) 数组或函数指针转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。一个数组实参可以转换为一个指向其首元素的指针。类似的,一个函数实参可以转换为一个该函数类型的指针。

      3) 算术转换:运算符的运算对象将转换成最宽的类型。(c++ primer P142

      4) 派生类向基类的转换:派生类向基类的隐式转换(包括动态绑定)。(c++ primer P530

      5) 用户定义的转换:类型转换函数的一般形式 "operator type() const;" (c++ primer P263&&P514


      其中,只有很有限的几种类型转换会自动地应用于函数实参。编译器通常不是对实参进行类型转换,而是生成一个新的模板实例:与往常一样,顶层const无论在形参还是实参中,都会被忽略。在其他类型转换中,能在调用中应用于函数模板的包括如下两项:

      1) const转换:可以将一个非const对象的引用(或指针)传递给一个const的引用(或指针)形参。

      2) 数组或函数指针转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。一个数组实参可以转换为一个指向其首元素的指针。类似的,一个函数实参可以转换为一个该函数类型的指针。

    2. 函数模板显式实参

      当函数返回类型与参数列表中任何类型都不相同时,经常会出现以下两种情况:

      1) 在某些情况下,编译器无法推断出模板实参的类型;

      2) 在一些情况下,我们希望允许用户控制模板实例化。

    举例:

    1 //编译器无法推断T1,它未出现在函数参数列表中
    2 template <typename T1,typename T2,typename T3>
    3 T1 sum(T2,T3)
    4 //T1是显式指定的,T2和T3是从函数实参类型推断而来的
    5 auto val3=sum<long long>(i,lng);  //long long sum(int,long)

      显式模板实参按由左至右的顺序与对应的模板参数匹配:第一个模板实参与第一个模板参数匹配,第二个实参与第二个参数匹配,一次类推。只有尾部(最右)参数的显式模板实参才可以忽略,而且前提是它们可以从函数参数中推断出来。

    1 template <typename T1,typename T2,typename T3>
    2 T3 sum(T2,T1);
    3 
    4 //错误:不能推断前几个模板参数
    5 auto val3=sum<long long>(i,lng);
    6 //正确:显式指定了所有三个参数
    7 auto val2=sum<int,long,long,long>(i,lng);

    3. 尾置返回类型与类型转换

      用途:当返回类型不确定时。

    1) 尾置返回类型

    举例:我们希望编写而一个函数,接受表示序列的一对迭代器和返回序列中一个元素的引用:由于尾置返回出现在参数列表之后,它可以使用函数的参数

    //尾置返回类型
    template <typename It>
    auto fcn(It beg,It end) ->decltype(*beg)
    {
        //处理序列操作
        return *beg;          //返回序列中一个元素的引用
    }
    
    //decltype(*beg) 不能写在auto 的位置,是因为在参数列表之前,beg 都是不存在的。

    2) 进行类型转换的标准库模板类
      所需头文件 #include<type_traits>

    //remove_reference::type脱去引用,剩下元素类型本身
    
    //为了使用模板参数的成员,必须使用typename
    template <typename It>
    auto fcn2(It beg,It end) -> 
            typename remove_reference<decltype(*beg)>::type   
    {
        //处理序列
        return *beg
    }
    
    //typename 告诉编译器,type表示一个类型

    4. 函数指针和实参推断

      当用一个函数模板初始化一个函数指针为一个函数指针赋值时,编译器使用指针的类型来推断模板实参。

     1 template <typename T> int compare(const T&,const T&);
     2 //pf1指向实例int compare(const int&,const int&);
     3 int (*pf1)(const int&,const int&)=compare;
     4 /****************************************/
     5 //func的重载版本
     6 func(int (*)(const int&,const int&));
     7 func(int (*)(const string&,const string&));
     8 //错误:使用compare的哪个实例
     9 func(compare);
    10 //正确:显式指出实例化哪个版本
    11 func(compare<int>);

      略(c++ primer P607)

    5. 模板实参推断和引用(比较绕)

    两个正常绑定规则:

    5.1 从左值引用函数参数推断类型

      1. 当一个函数参数是模板类型参数(T) 的一个普通(左值)引用时(T&),绑定规则告诉我们,只能传递给它一个左值(如一个变量或一个返回引用类型的表达式)。实参可以是const类型,也可以不是。

      2. 如果一个函数参数的类型是const T&,可以传递给它任何类型的实参--一个对象(const或非const)、一个临时对象或是一个字面值常量值。

    //1.
    template <typename T>
    void f1(T&);        //实参必须是一个左值
    
    f1(i);                  //i是一个int;模板参数类型T是int
    f1(ci);                //ci是一个const int;模板参数T是const int
    f1(5);                //错误:传递给一个&参数的实参必须是一个左值
    
    //2.
    template <typename T>
    void f2(const T&);      //可以接受一个右值
    
    f2(i);                 //i是一个int;模板参数T是int
    f2(ci);               //ci是一个const int,但模板参数T是int
    f2(5);                //一个const & 参数可以绑定道一个右值;T是int

    5.2 从右值引用函数参数推断类型

      当一个函数参数是一个右值引用(T&&)时,可以传递给它一个右值。

    1 template <typename T>
    2 void f3(T&&);
    3 
    4 f3(42);    //实参是一个int;模板参数T 是int

    两条例外规则

      C++在正常绑定规则之外定义了两个例外规则,允许一个右值引用绑定到一个左值上。这连个例外规则是move这种标准库设施正确工作的基础。

      1. 第一个例外规则影响右值引用参数的推断如何进行。当我们将一个左值(如i )传递给函数的右值引用参数,且此右值引用指向模板类型参数(如T&&)时,编译器推断推断模板类型参数为实参的左值引用类型。因此,当我们调用f3(i)时,编译器推断T的类型为int&,而非int。

      2. 第二个规则:如果我们间接创建一个引用的引用,则这些引用形成了“折叠”。在所有情况下(除第一个例外),引用会折叠成一个普通的左值引用类型。在新标准下,折叠规则扩展到右值引用。只在一种特殊情况下引用会折叠成右值引用:右值引用的右值引用。即对于一个给定类型X:

    1) X& &、X& &&和X&& &都折叠成类型X&;

    2) 类型X&& &&折叠成X&&。

      这两个规则导致了两个重要结果

      1. 如果一个函数参数是 "T&&",则它可以被绑定到一个左值;且

      2. 如果实参是一个左值,则推断出的模板实参类型将是一个左值引用,且函数参数将被实例化为一个(普通)左值引用参数(T&)。

     1 template <typename T>
     2 void f3(T&& val)
     3 {
     4     T t=val;   //拷贝还是绑定一个引用
     5     t=100;
     6     if(val==t)
     7         cout<<"绑定一个引用"<<endl;
     8     else
     9         cout<<"拷贝"<<endl;
    10 }
    11 
    12 int main()
    13 {
    14     int i=10;
    15     f3(i);     //推断T为int&,因此T&&是int& &&,会折叠为int&
    16     f3(10);    //推断T为int
    17     return 0;
    18 }

      注意15行注释:推断T为int&,因此T&&是int& &&,会折叠为int&.

      在实际中,右值引用通常用于两种情况:模板转发其实参或模板被重载。使用右值引用的函数模板通常使用(P481)中介绍的方式进行重载:

    1 template <typename T> void f(T&&);        //绑定到非const右值
    2 template <typename T> void f(const T&); //绑定到左值和const右值

      略(c++ primer P608)

     六、重载与模板

      函数模板可以被另一个模板或一个普通非模板函数重载。与往常一样,名字相同的函数必须具有不同数量或类型的参数。

      函数匹配规则:《C++ primer》 P615,值得注意的是,如果有多个函数提供同样好的匹配,则:

      --若同样好的函数中只有一个是非模板函数,则选择此函数;(同等条件下,优先选非模板函数

      --若同样好的函数中没有非模板函数,而有多个函数模板,且其中一个模板比其他模板更特例化,则选择此模板;(同等条件下,选择更特例化的函数模板

      --否则,此调用有歧义。

     1 #include <iostream>
     2 #include <sstream>
     3 using namespace std;
     4 
     5 template <typename T>
     6 string debug_rep(const T &t)
     7 {
     8     cout<<"debug_rep版本"<<endl;
     9     ostringstream ret;
    10     ret<<t;
    11     return ret.str();
    12 }
    13 template <typename T>
    14 string debug_rep(T *p)
    15 {
    16     cout<<"debug_rep1版本"<<endl;
    17     ostringstream ret;
    18     ret<<"pointer:"<<p; //打印指针本身的值
    19     if(p)
    20         ret<<" "<<debug_rep(*p);
    21     else
    22         ret<<"null pointer";
    23     return ret.str();
    24 }
    25 //非模板和模板重载
    26 string debug_rep(const string &s)
    27 {
    28     cout<<"非模板函数版本"<<endl;
    29     return '"'+s+'"';   //
    30 }
    31 int main()
    32 {
    33     string s("hello");
    34     cout<<debug_rep(s)<<endl<<endl;
    35     cout<<debug_rep(&s)<<endl<<endl;
    36 
    37     const string *sp=&s;
    38     cout<<debug_rep(sp)<<endl<<endl;  //多个函数模板,优先匹配"更特例化的模板"
    39 
    40     string s1("hi");
    41     cout<<debug_rep(s1)<<endl<<endl;  //同等条件下,优先匹配"非模板函数"
    42     return 0;
    43 }
    View Code

  • 相关阅读:
    Access小用之感
    PHP学习之路今日开启
    IBatis初体验2
    PHP之PDO介绍
    javascript key code 大全
    如何将虚拟机中的Linux系统与shell终端连接
    linux磁盘情况查询
    vi和vim编辑器的使用
    Linux磁盘分区,挂载
    (转)Android Dalvik虚拟机初识
  • 原文地址:https://www.cnblogs.com/cygalaxy/p/6881868.html
Copyright © 2020-2023  润新知