• 第16章模板与泛型编程


    一、函数模板

    template <typename T1,typename T2>
    int function(const T1 & t1,const T2 &t2){}

    尖括号内部的参数叫模板参数。

    1.实例化函数模板

    编译器用函数实参推断模板实参。这些编译器生成的版本被称为模板的实例。

    2.模板类型参数

    模板类型参数是可以用来表示返回类型或函数参数类型,以及在函数体内用于变量声明或类型转换的参数,如下程序所示。必须说明非类型模板参数并不是表示类型的参数,而是表示具体数值的参数,详见下文。

    template <typename T>
    T foo(T *p)
    {
        T tmp=*p;
        return tmp;
    }

    3.非类型模板参数

    非类型模板参数表示一个值不是一个类型!!通过定义特定的类型名来指定参数,而不是typename或class。

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

    非类型模板参数的模板实参必须是常量表达式!!

    template <unsigned N,unsigned M>
    int compare(const char (&p1)[N],const char (&p2)[M])
    {
        return strcmp(p1,p2);
    }

    当调用compare("hi","mom")时,会实例化如下版本:

    int compare(const char (&p1)[3],const char (&p2)[4])

    一个f非类型参数可以是整型,或一个指向对象或函数类型的指针或(左值)引用。绑定到非类型整型参数的实参必须是一个常量表达式。绑定到指针或引用非类型参数的实参必须具有静态生存期(static)。

    4.模板编译

    当编译器遇到一个模板定义时,并不生成代码,只有实例化模板后,编译器才会生成代码。

    模板为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,模板的头文件通常既包括声明也包括定义。

    二、类模板

    template <typename T>
    class blob
    {
    public:
        T a;
        T *P
    };

    1.实例化类模板

    类模板都必须显示实例化,使编译器使用实例化后的类型,例 blob<int>.

    2.类模板成员函数

    (1)普通成员函数

    类内定义,与普通函数定义一致。

    类外定义,与普通类成员函数定义一致。

    (2)模板成员函数

    即成员函数是模板函数。

    类内定义,与普通模板函数定义一致,如下:

    template <typename T>//定义模板类
    class blob
    {
    public:
        template <typename U> int fun(const U &u1){};//定义成员模板函数
        T a;
        T *P
    };

    当然,模板函数的形参类型也可以与类模板参数一样。

    类外定义,需要同时声明两个模板参数,即

    template <typename T>
    template <typename U>
    int blob<T>::fun(const U &u1){}

    3.类模板与友元

    (1)一对一友好关系

    类模板与另一个(类或函数)间友好关系的最常见形式是建立对应实例及其友元间的友好关系。

    为了引用(类或函数)模板的一个特定实例,必须首先声明模板自身,如下:

    //前置声明,在blob中声明友元所需要的
    template <typename > class blobptr;
    template <typename > class blob;
    template <typename T>
    bool operator==(const blob<T>&,const blob<T>&);
    //每个blob实例将访问权限授予相同类型实例化的blobptr和相等运算符
    template <typename T> class blob
    {
        friend class blobptr<T>;
        friend bool operator==<T>(const blob<T>&,const blob<T>&);
    };

    (2)通用和特定的模板友好关系

    template <typename T>class pal;
    template <typename T>class C
    {
        //C的每个实例将相同实例化pal声明为友元
        friend class pal<T>;
        //pal2的所有实例都是C的每个实例的友元,不需要前置声明
        template <typename X> friend class pal2;
        //pal3是一个非模板类,它是C所有实例的友元
        friend class pal3;
    };

    (3)模板参数

    由于c++默认的作用域访问说明符为名字而非类型,因此若要使用其表示类型需要使用typename关键字。

    4.控制实例化或

    当两个或多个独立编译的源文件使用了相回的模板,并提供了相回的模板参数时,每文性中就都会有该模板的一个实例。在大系统中,在多个文件中实例化相同模板的额外开销可能非常严重。在新标准中,我们可以通过显式实例化(explicit instantiation)来避免这种开销。一个显式实例化有如下形式:

    extern template declaration;//实例化声明
    template declaration;//实例化定义

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

    extern template class blob<string>;//实例化声明
    template int compare(const int &,const int&);//实例化定义

    当编译器遇到extern模板声明时,它不会在本文件中生成实例化代码。将一个实例化声明为extern就表示承诺在程序其他位置有该实例化的一个非extern声明(定义)。当编译器遇到一个实例化定义时,它为其生成代码。另外,实例化定义会实例化该模板所有成员。

    三、模板实参推断

    1.类型转换

    类型转换能在调用中应用与函数模板的包括如下两项:

    • const转换。顶层const无论在形参中还是实参中都会被忽略。可以将一个非const对象的引用(或指针)传递给一个const的引用(或指针)形参。
    • 数组或函数指针转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。一个数组实参可以转换为一个指向其首元素的指针。一个函数实参可以转换为一个该函数类型的指针。
    • 若函数除了模板形参外,还有特定类型的形参,则该实参可以进行正常的类型转换。

    2.函数模板显式实参

    与类模板一样,可以定义表示返回类型的第三个模板参数,从而允许用户控制返回类型。

    template <typename T1,typename T2,typename T3>
    T1 sum(T2,T3);
    
    int i;
    long lng;
    //T1是显式指定的,T2和T3是从函数实参类型推断而来的
    auto val=sum<long long>(i,lng);//long long sum(int,long)

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

    如果我们希望编写一个函数,接受表示序列的一对迭代器和返回序列中一个元素的引用:

    template <typename T>
    ??? &fcn(T beg,T end)
    {
        //处理序列
        return *beg;//返回序列中一个元素的引用
    }

    我们并不知道结果的准确类型,但知道所需类型是所处理的序列的元素类型。为此,需要使用尾置返回类型,即

    template <typename T>
    auto fcn(T beg,T end) -> decltype(*beg)
    {
        //处理序列
        return *beg;//返回序列中一个元素的引用
    }

    此例中利用decltype和beg解引用,返回引用的元素类型。那如果需要返回元素类型,而不是元素类型的引用,就需要用到进行类型转换的标准库模板类

    remove_reference,它在头文件type_traits中。

    template <typename T>
    auto fcn(T beg,T end) -> typename remove_reference<decltype(*beg)>::type //typename是为了声明type是一个类型而非名字
    {
        //处理序列
        return *beg;//返回序列中一个元素的引用
    }

    4.函数指针和实参推断

    template <typename T>
    int compare(const T&, const T&);
    //pf1指向实例int compare(const int&,const int&)
    int (*pf1)(const int&, const int &)=compare;

    pf1中参数类型决定了T的模板实参类型。指针PF1指向compare的int版本实例。

    5.模板实参推断和引用

    (1)左值引用函数参数推断类型

    当一个函数参数是模板类型参数的一个普通(左值)引用时,绑定规则告诉我们,只能传递给他一个左值(如变量或一个返回引用类型的表达式)。如果实参是const的,则T将被推断为const类型。例:

    template <typename T>
    void f(T&);//实参必须是一个左值
    f(i);//i是一个int类型;模板参数类型T是int
    f(ci);//ci是一个const int;模板参数T是const int
    f(5);//错误
    如果一个函数参数的类型是const T&,正常的绑定规则告诉我们可以传递给它任何类型的实参(一个对象(const或非const)、一个临时对象或是一个字面常量值)。当
    函数参数本身是const时,T的类型推断的结果不会是一个const类型。const已经是函数参数类型的一部分;因此,它不会也是模板参数类型的一部分:
    template <typename T>
    void f(const T&);//可以接受一个右值
    f(i);//i是一个int类型;模板参数类型T是int
    f(ci);//ci是一个const int;模板参数T是int
    f(5);//一个const &参数可以绑定到一个右值;T是int

    (2)从右值引用函数参数推断类型

    当一个函数参数是一个右值引用时,正常绑定规则告诉我们可以传递给他一个右值。当我们这样做时,类型推断过程类似普通左值引用函数参数的推断过程。
    推断出的T的类型是该右值实参的类型:
    template <typename T>
    void f1(T&&);//可以接受一个右值
    f1(5);//实参是一个int类型的右值;模板参数T是int

    (3)引用折叠和右值引用参数

    • 如果一个函数参数是一个指向模板类型参数的右值引用(如,T&&),则它可以被绑定到一个左值;
    • 且如果实参是一个左值,则推断出的模板实参类型将是一个左值引用,且函数参数将被实例化为一个(普通)左值引用参数(T&)

    右值引用通常用于两种情况:模板转发其实参或模板被重载。

     6.转发(保持类型信息)

    • 利用右值引用,引用折叠保持其对应实参的所有类型信息。
    • 利用头文件utility中的forward<T>保持模板函数内传递给其他函数的实参类型。forward<T>返回类型是T&&。

    例:

    template <typename T>
    void f1(T&& t1);
    {
        f2(std::forward<T>(t1));
    }

    四、重载与模板

    函数匹配规则:

    • 对于一个调用,其候选函数包括所有模板实参推断(参见16.2节,第600页)成功的函数模板实例。
    • 候选的函数模板总是可行的,因为模板立参推断会排除任何不可行的模板。
    • 与在常一样,可行函数(模板与非械板)按类型转换(如果对此调用需要的话)来排序。当然,可以用于函数模板调用的类型转换是非常有限的(参见16.2.1节,第
    601页)。
    • 与往常一样,如果恰有一个函数提供比任何其他函数都更好的匹配,则选择此函数。但是,如果在多个函数提供同样好的匹配,则:
         ——如果同样好的函数电只有一个是非模板函数,则选择此函数。
    ——如果同样好的函数中没有韭模板函数,而有多个函数模板,且其中一个模板比其他模板更特例化,则选择此模板。
    否列,此调用有岐义。

    五、可变参数模板

    一个可变参数模板就是一个接受可变数目参数的模板函数或模板类。可变数目的参数称为参数包。存在两种参数包:模板参数包,函数参数包

    用一个省略号来指出模板参数或函数参数表示一个包。

    template <typename T,typename ...arg>//arg是一个模板参数包,表示零个或多个
    void foo(const T&t, const arg&... rest);//rest是一个函数参数包,表示零个或多个

    (1)sizeof...运算符

    当我们需要知道包中有多少元素时,可以使用sizeof...运算符。

    template <typename T,typename ...arg>//arg是一个模板参数包,表示零个或多个
    void foo(const T&t, const arg&... rest)//rest是一个函数参数包,表示零个或多个
    {
        cout<< sizeof...(arg)<<endl;//类型参数的数目
        cout<< sizeof...(rest)<<endl;//函数参数的数目
    }

    (2)编写可变参数函数模板

    如6.2.6节(第198页)所述,我们可以使用一个initializer 1ist来定义一个可接受可变数目实参的函数。但是,所有实参必须具有相同的类型
    (或它们的类型可以转
    换为同一个公共类醇)。当我们既不知道想要处理的实参的数目也不知道它们的类型时,可变参数通数是很有用的。作为一个例子,
    我们将定义一个函数,它类似较早的
    error_msg函数,差别仅在于新函数实参的类型也是可变的。我们首先定义一个名为print的函数,它在一个给定流上
    打印给定实参列表的内容。

    可变参数函数通常是递归的。第一步调用处理包中的第一个实参,然后用剩余实参调用自身。
    我们的print函数也是这样的模式,每次递归调用将第二个实参打印到第一个实参表示的流中。
    为了终止递归,我们还需要定义一个非可变参数的print函数,它接受一个流和一个对象:
    //用来终止递归并打印最后一个元素的函数
    template <typename T>
    ostream &print(ostream &os,const T&t)
    {
        return os<<t;
    }
    //包中除了最后一个元素外的其他元素都会调用这个版本的print
    template <typename T,typename ...arg>
    ostream &print(ostream &os,const T&t, const arg&... rest)
    {
       cout<<t<<",";
        return print(os,rest...);
    }

    六、模板特例化

    template<>进行模板特例化

    template <>
    ostream &print(ostream &os,const int &t)
    {
        return os<<t;
    }

    函数模板必须全部特例化,类模板可以部分特例化。

  • 相关阅读:
    剑指offer-23.链表中环的入口节点
    剑指offer-6从尾到头打印链表
    剑指offer-24.反转链表
    2-常见机器学习模型总结
    1-预测分析类核心算法简介
    罗马数字转整数Leetcode13
    链表反转leetcode206
    LINUX常用命令
    两种遍历list
    python笔记-字符串函数总结
  • 原文地址:https://www.cnblogs.com/excellentlhw/p/10299537.html
Copyright © 2020-2023  润新知