第16章模板与泛型编程

一笑奈何 提交于 2019-12-21 03:36:21

一、函数模板

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;
}

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

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!