【C++知识】模板与泛型编程

前言

         一个模板就是一个编译器生成特定类类型或函数的蓝图。生成特定类或函数的过程称为实例化。我们只编写一次模板,就可以将其用于多种类型和值,编译器会为每种类型和值进行模板实例化。这一章内容有点儿多,需要大家慢慢看和理解,需要了解更多详细知识,建议自行查看书籍,这里主要介绍一些细节。

最后,如果有理解不对的地方,希望大家不吝赐教,谢谢!

十三、模板与泛型编程

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

       模板是C++中泛型编程的基础,一个模板就是一个创建类或函数的蓝图或者说公式。

定义模板

函数模板

       模板定义以关键字template开始,后跟一个模板参数列表,这是一个逗号分隔的一个或多个模板参数的列表,用<>包围起来。在模板定义中,模板参数列表不能为空。

       模板参数表示在类或函数定义中用到的类型或值,当使用模板时,我们(隐式地或显式地)指定模板参数,将其绑定到模板参数上。

template
int compare(const T &v1,const T &v2)
{
    if(v1

      我们的compare函数声明了一个名为T的类型参数,在compare中,我们用名字T表示一个类型。而T表示的实际类型则在编译时根据compare的使用情况来确定。

      编译器用推断出的模板参数来为我们实例化一个特定版本的函数。当编译器实例化一个模板时,它使用实际的模板实参代替对应的模板参数来创建出模板的一个新“实例”。

模板类型参数

      一般来说,我们可以将类型参数看作类型说明符,就像内置类型或类类型说明符一样使用。特别是,类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明类型转换类型参数前必须使用关键字class或typename在模板参数列表中,这两个关键字含义相同,可以互相使用,一个模板参数列表中可以同时使用这两个关键字。建议使用typename.

非类型模板参数

       除了定义类型参数,还可以在模板中定义非类型参数,一个非类型参数表示一个而非一个类型。我们通过一个特定的类型名而非关键字class或typename来指定非类型参数。

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

inline和constexpr的函数模板

     函数模板可以声明为inline或constexpr的,如同非模板函数一样,inline或constexpr说明符放在模板参数列表之后,返回类型之前。

编写类型无关的代码

       模板程序应该尽量减少对实参类型的要求。

模板编译

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

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

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

类模板

      类模板是用来生成类的蓝图的。与函数模板不同之处是,编译器不能为类模板推断模板参数类型,为了使用类模板,我们必须在模板名后的尖括号中提供额外信息——用来代替模板参数的模板实参列表。

定义类模板

      类似函数模板,类模板以关键字以关键字template开始,后跟模板参数列表。在类模板(及其成员)的定义中,我们将模板参数当作替身,代替使用模板时用户需要提供的类型或值。

实例化类模板

       当使用一个类模板时,我们必须提供额外信息。我们现在知道额外信息是显式模板实参列表,它们被绑定到模板参数,编译器使用这些模板实参来实例化出特定的类。

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

      无论何时使用模板都必须提供模板实参。

类模板的成员函数

      与其他任何类相同,我们既可以在类模板内部,也可以在类模板外部为其定义成员函数,且定义在类模板内的成员函数被隐式声明为内联函数。

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

ret-type class-name::member-name(parm-list) 

      默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化。当我们处于一个类模板的作用域中时,编译器处理模板自身引用时就好像我们已经提供了与模板参数匹配的实参一样。

在一个类模板的作用域内,我们可以直接使用模板名而不必指定模板实参。

类模板与友元

        当一个类包含一个友元声明时,类与友元各自是否是模板是相互无关的。如果一个类模板包含一个非模板友元,则友元被授权可以访问所有模板实例。如果友元自身是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例。

一对一友好关系

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

template class Blob
{
    friend class BlobPtr;
};

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

       一个类也可以将另一个模板的每个实例都声明为自己的友元,或者限定特定的实例为友元:

模板类型别名

template using twin=pair;
twin authors;  //authors是一个pair

模板参数

      类似于函数参数的名字,一个模板参数的名字也没有什么内在含义。我们通常将类型参数命名为T,但实际上我们可以使用任何名字。

模板参数与作用域

      模板参数遵循普通的作用域规则。在模板内不能重用模板参数名。由于参数名不能重用,所以一个模板参数名在一个特定模板参数列表中只能出现一次。

模板声明

     模板声明必须包含模板参数。与函数参数相同,声明中的模板参数的名字不必与定义中相同。当然,一个给定模板的每个声明和定义必须有相同数量和种类的参数。

     一个特定文件所需要的所有模板的声明通常一起放置在文件开始位置,出现于任何使用这些模板的代码之前。

使用类的类型成员

       默认情况下,C++语言假定通过作用域运算符访问的名字不是类型。因此,如果我们希望使用一个模板类型参数的类型成员,就必须显式告诉编译器该名字是一个类型。当我们希望通知编译器一个名字表示类型时,必须使用关键字typename,而不能使用class。

默认模板实参

template >
int compare(const T &v1,const T &v2,F f=F())
{
    if(f(v1,v2)) return -1;
    if(f(v2,v1)) return 1;
    return 0;
}

   模板默认实参与类模板 

         如果一个类模板参数都提供了默认实参,且我们希望使用这些默认实参,就必须在模板名之后跟一个空尖括号对。

成员模板

      一个类可以包含本身是模板的成员函数,这种成员被称为成员模板。成员模板不能是虚函数。

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

     与任何其他模板相同,成员模板也是以模板参数列表开始的。

类模板的成员模板

      对于类模板,我们也可以为其定义成员模板。在此情况下,类和成员各自有自己的、独立的模板参数。当我们在类模板外定义一个成员模板时,必须同时为类模板和成员模板提供模板参数列表。类模板的参数列表在前,后跟成员自己模板参数列表

template  //类的类型参数
template  //构造函数的类型模板
Blob::Blob(It b,It e){}

 控制实例化

        在大系统中,在多个文件中实例化相同模板的额外开销可能严重。在新标准中,我们可以通过显式实例化来避免这种开销。一个显式实例化有如下形式:

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

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

//实例化声明与定义
extern template class Blob;  //声明
template int compare(const int&,const int&);  //定义

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

由于编译器在使用一个模板时自动对其实例化,因此extern声明必须出现在任何使用此实例化版本的代码之前

对每个实例化声明,在程序中某个位置必须有其显式的实例化定义。

实例化定义会实例化所有成员

     一个类模板的实例化会实例化该模板的所有成员,包括内联的成员函数。编译器会实例化该类的所有成员,即使我们不使用某个成员,它也会被实例化。因此,我们用来显式实例化一个类模板的类型,必须能用于模板的所有成员。

     在一个类模板的实例化定义中,所用类型必须能用于模板的所用成员函数。

效率与灵活性

       对模板设计者所面对的设计选择,标准库智能指针类型给出了一个很好的展示。

      shared_ptr和unique_ptr之间的明显不同是:前者给予我们共享指针所有权的能力;后者则独占指针。另一个差异是:我们可以很容易地重载一个shared_ptr的删除器,只要在创建或reset指针时传给它一个可调用对象即可。与之相反,删除器的类型是一个unique_ptr对象的类型的一部分。用户必须在定义unique_ptr时以显式模板的形式提供删除器的类型。因此,对于unique_ptr的用户来说,提供自己的删除器就更为复杂。

在运行时绑定删除器

      shared_ptr必须能直接访问其删除器,即,删除器必须保存为一个指针或一个封装了指针的类。

      我们可以确定shared_ptr不是将删除器直接保存为一个成员,因为删除器的类型直到运行时才会知道。实际上,在一个shared_ptr的生存期中,我们可以随时改变其删除器类型。通常,类成员的类型在运行时是不能改变的,因此,不能直接保存删除器。

在编译时绑定删除器

       对于unique_ptr,删除器的类型是类类型的一部分,即unique_ptr有两个模板参数,一个表示它所管理的指针,另一个表示删除器的类型。由于删除器的类型是unique_ptr类型的一部分,因此删除器成员的类型在编译时就知道的,从而删除器直接保存在unique_ptr对象中。

通过在编译时绑定删除器,unique_ptr避免了间接调用删除器的运行时开销。通过在运行时绑定删除器,shared_ptr使用户重载删除器更为方便。 

模板实参推断

       对于函数模板,编译器利用调用中的函数实参来确定其模板参数。从函数实参来确定模板实参的过程被称为模板实参推断。 在模板实参推断过程中,编译器使用函数调用中的实参类型来寻找模板实参,用这些模板实参生成的函数版本与给定的函数调用最为匹配。

类型转换与模板类型参数

       能在调用中应用于函数模板的包括如下两项:

  • const转换:可以将一个非const对象的引用(或指针)传递给一个const的引用(或指针)形参
  • 数组或函数指针的转换:一个数组实参可以转换为一个指向其首元素的指针。类似的,一个函数实参可以转换为一个该函数类型的指针。

将实参传递给带模板类型的函数形参时,能够自动应用的类型转换只有const转换及数组或函数到指针的转换。

使用相同模板参数类型的函数形参

      传递给这些形参的实参必须具有相同的类型。

正常类型转换应用于普通函数实参

       函数模板可以有用普通类型定义的参数,即不涉及模板类型参数的类型,这种函数实参不进行特殊处理,它们正常转换为对应形参的类型。

函数模板显式实参

       在某些情况下,编译器无法推断出模板实参的类型。其他一些情况下,我们希望允许用户控制模板实例化。当函数返回类型与参数列表中任何类型都不相同时,这两种情况最常出现。

指定显式模板实参

//编译器无法推断T1,它未出现在函数参数列表中
template 
T1 sum(T2,T3);

   没有任何函数实参的类型可用来推断T1的类型,每次调用sum时调用者都必须为T1提供一个显式模板实参。显式模板实参在尖括号中给出,位于函数名之后,实参列表之前:   

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

显式模板实参按由左至右的顺序与对应的模板参数匹配,第一个模板实参与第一个模板参数匹配,第二个实参与第二个参数匹配。

 尾置返回类型与类型转换

//尾置返回允许我们在参数列表之后声明返回类型
template
auto fcn(It beg,It end)->decltype(*beg)
{
    //处理序列
    return *beg;   //返回序列中一个元素的引用
}

当参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数,能唯一确定其值类型或值。 

模板实参推断和引用

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

       当一个函数参数是模板类型参数的一个普通(左值)引用时,绑定规则告诉我们,只能传递给它一个左值。实参可以是const类型,也可以不是。如果实参是const的,则T将被推断为const类型。

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

       当一个函数参数是一个右值引用时,正常绑定规则告诉我们可以传递给它一个右值。当我们这样做时,类型推断过程类似普通左值引用函数参数的推断过程。推断出的T的类型是该右值实参的类型。

引用折叠和右值引用参数

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

  • X& &、X& &&和X&& &都折叠成类型X&
  • 类型X&& &&折叠成X&&

引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数。

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

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

另外值得注意的是,这两个规则暗示,我们可以将任意类型的实参传递给T&&类型的函数参数。对于这种类型的参数,(显然)可以传递给它右值,而如我们刚刚看到的,也可以传递给它的左值。

     虽然不能隐式地将一个左值转换为右值引用,但我们可以用static_cast显式地将一个左值转换为一个右值引用。对于操作右值引用的代码来说,将一个右值引用绑定到一个左值的特性允许它们截断左值。

转发

      某些函数需要将其一个或多个实参连同类型不变地转发给其他函数。在此情况下,我们需要保持被转发实参的所有性质,包括实参类型是否是const的以及实参是左值还是右值。

定义能保持类型信息的函数参数

       为了通过翻转函数传递一个引用,我们需要重写函数,使其参数能保持给定实参的“左值性”。更进一步,可以想到我们也希望保持参数的const属性。

       通过将一个函数参数定义为一个指向模板类型参数的右值引用,我们可以保持其对应实参的所有类型信息。而使用引用参数(无论是左值还是右值)使得我们可以保持const属性,因为在引用类型中的const是底层的。如果我们将函数参数定义为T1&&和T2&&,通过引用折叠就可以保持翻转实参的左值/右值属性。

      如果一个函数参数是指向模板类型参数的右值引用(如T&&),它对应的实参的const属性和左值/右值属性将得到保持。

重载与模板

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

       如果涉及函数模板,则函数匹配规则会在以下几个方面受到影响:

  • 对于一个调用,其候选函数包括所有模板实参推断成功的函数模板实例。
  • 候选的函数模板总是可行的,因为模板实参推断会排除任何不可行的模板。
  • 可行函数(模板与非模板)按类型转换(如果对此调用需要的话)来排序。当然,可以用于函数模板调用的类型转换是非常有限的。
  • 如果恰有一个函数提供比其他函数都更好的匹配,则选择此函数。

正确定义一组重载的函数模板需要对类型间的关系及模板函数允许的有限的实参类型转换有深刻的理解。

       在定义任何函数之前,记得声明所有重载函数版本。这样就不必担心编译器由于未遇到你希望调用的函数而实例化一个并非你所需的版本。 

可变参数模板

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

       我们用一个省略号来指出一个模板参数或函数参数表示一个包。在一个模板参数列表中,class...或typename...指出接下来的参数表示零个或多个类型的列表;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。在函数参数列表中,如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包。

sizeof...运算符

     当我们需要知道包中有多少元素时,可以使用sizeof...运算符。返回一个常量表达式,而且不会对其实参求值。

包扩展

      对于一个参数包,除了获取其大小外,我们能对它做的唯一的事情就是扩展它。当扩展一个包时,我们还要提供用于每个扩展元素的模式。扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式右边放一个省略号(...)来触发扩展操作。

扩展中的模式会独立地应用于包中的每个元素。

模板特例化

     在某些情况下,通用模板的定义对特定类型是不适合的:通用定义可能编译失败或做得不正确。其他时候,我们也可以利用某些特定知识来编写更高效的代码,而不是从通用模板实例化。当我们不能使用模板版本时,可以定义类或函数的一个特例化版本。

定义函数模板特例化

      当我们特例化一个函数模板时,必须为原模板中的每个模板参数都提供实参。为了指出我们正在实例化一个模板,应使用关键字template后跟一个空尖括号对(<>)。空尖括号指出我们将为原模板的所有模板参数提供实参。

函数重载与模板特例化

       一个特例化版本本质上是一个实例,而非函数名的一个重载版本。因此,特例化不影响函数匹配。

模板及其特例化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前面,然后是这些模板的特例化版本。

类模板特例化

     必须在原模板定义所在的命名空间特例化它。我们只需要知道——我们可以向命名空间添加成员。为了达到这一目的,首先必须打开命名空间。

类模板部分特例化

       与函数模板不同,类模板的特例化不必为所有模板参数提供实参。我们可以只指定一部分而非所有模板参数,或是参数的一部分而非全部特性。一个类模板的部分特例化本身是一个模板,使用它时用户还必须为那些在特例化版本中未指定的模板实参提供实参。

我们只能部分特例化类模板,而不能部分特例化函数函数模板。

特例化成员而不是类

      我们可以只特例化特定成员函数而不是特例化整个模板。

你可能感兴趣的:(C++,C++,模板与泛型编程)