模版与泛型编程

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

模版是C++泛型编程的基础。一个模版就是一个创建类或函数的蓝图或者说公式。当使用一个vector这样的泛型类型,或者find这样的泛型函数时,我们提供足够的信息,将蓝图转换为特定的累活函数。这种转换发生在编译时。

定义模版:

函数模版:模版定义以关键字template开始,后跟一个模版参数列表,这是一个逗号分隔的一个或多个模版参数的列表,用“< >”包围起来。

模版参数表示在类或函

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

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

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

非类型模版参数:非类型模版参数表示一个一个值而非一个类型。我们通过一个特定的类型名而非关键字class或typeneme来指定非类型参数。当一个模版被实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替。这些值必须是常量表达式,从而允许编译器在编译时实例化模版。

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

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

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

模版编译:当编译器遇到一个模版定义时,他并不产生代码。只有当我们实例化出模板的一个特定版本时,编译器才会产生代码。当我们使用而不是定义模版时,编译器才生成代码,这一特性影响了我们如何组织代码以及错误何时被检测到。

通常,当我们调用一个函数时,编译器只需要掌握函数的声明。类似的,当我们使用一个类类型的对象时,类定义必须是可用的,但是成员函数的定义不必已经出现,因此我们将类定义和函数声明放在头文件中,而普通函数和类的成员函数的定义放在源文件中。

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

模版包括两种名字:

1、那些不依赖于模版参数的名字;

2、那些依赖于模版参数的名字;

当使用模版时,所有不依赖于模版参数的名字都必须是可见的,这是由模板的提供者来保证的。而且,模板的提供者必须保证,当模版被实例化时,模板的定义,包括类模板的成员的定义,也必须是可见的。

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

类模版:与函数模板的不同之处是:编译器不能为类模版推断模版参数类型。为了实用类模版,我们必须在模版名后的尖括号中提供额外信息——用来替代模版参数的模版实参列表。这些额外信息被称为显示模版实参列表,他们被绑定到模版参数。一个类模版的每个实例都形成一个独立的类。类模版的每个实例都有其自己的版本的成员函数。因此,类模版的成员函数具有与模版相同的模版参数。因而,定义在类模版之外的成员函数就必须以关键字template开始,后接类模版参数列表。

默认情况下,一个类模版的成员函数只有当程序用到他时才进行实例化,这一特性使得即使某种类型不能完全符合模版操作的要求,我们仍能用该类型实例化类。

在类模版自己的作用域中,我们可以直接使用模版名而不提供实参。

当我们处于一个类模版的作用域中时,编译器处理模版自身引用时就好像我们已经提供了与模版参数匹配的实参一样。

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

为了让所有实例成为友元,友元声明中必须使用与类模版本身不同的模版参数。

值得注意的是,虽然友元通常来说应该是一个类或者一个函数,但是,我们完全可以用一个内置类型来实例化一个类模版,这种与内置类型的友元关系是允许的,以便我们能用内置类型来实例化这样的类。

和其他任何类型一样,我们可以定义一个typedef来引用实例化的类,由于模版不是一个类型,所以我们不能定义一个typedef引用一个模版,但是新标准允许我们为类模版定义一个类型别名。

类模版可以声明static成员。和其他static数据成员相同,模版类的每个static数据成员必须有且仅有一个定义。但是,模版类的每个实例都有一个独有的static对象。因此,与定义模板的成员函数类似,我们将static数据成员也定义成模版。

简单来说,函数模版是可以实例化出特定函数的模版,类模版是可以实例化出类的模版,在使用上,编译器会根据调用来为我们推断出函数模板的模版参数类型,而使用类模版的实例化特定类就必须显示指定模版参数。

模版参数与作用域:模版参数遵循普通的作用域规则。一个模版参数名的可用范围是在其生命之后,至模版声明或定义结束之前,与其他任何名字一样,类模版会隐藏外层作用域中声明的相同名字。但是,不同的是,在模版内不能重用模版参数名。由于不能重用参数名,所以一个模版参数名在一个特定模版参数列表中只出现一次。

声明时与函数参数相同,声明中的模版参数名字不必与定义中的相同。

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

默认情况下,C++语言假定通过作用域访问的名字不是类型,因此,如果我们希望使用一个模版类型参数的类型成员,就必须显示告诉编译器该名字是一个类型,通过使用关键字typename来实现:typename T::value_type();。

我们也可以为类模版提供默认模版实参。

当用来声明模版类型参数时,typename和class是完全等价的,都表明模版参数是一个类型。在C++最初引入模板时,是使用class的,但是为了避免与类定义中的class相混淆,引入了typename关键字,从字面上看typename还暗示了模版类型参数不必是一个类类型,因此现在更建议使用typename。如上文所述,typename还有其他用途,当在模版类型参数上使用作用域运算符来访问其成员时,在实例化之前有可能无法辨别访问的到底是静态成员还是类型成员。对此C++默认通过“::”访问的是静态成员,为了指明访问的是类型还是类型成员,需要在名字前使用typename关键字。一个类可以包含本身是模板的成员函数。这种成员被称为成员模版。成员模版不能是虚函数。

为了实例化一个类模版的成员模版,我们必须同时提供类和函数模板的实参。

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

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

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

一个类模版的实例化定义会实例化该模版的所有成员,包括内联的成员函数。在一个类模版的实例化定义中,所用类型必须能用于模板的所有成员函数。

模版实参推断:

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

编译器通常不是对实参进行类型转换,而是生成一个新的类型模版。

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

在某些情况下,编译器无法推断出模版实参的类型。其他一些情况下,我们希望允许用户控制模版实例化:指定显示模版实参:我们提供显示模版实参的方式与定义类模版实例的方式相同。显式模版实参在尖括号中给出,位于函数名之后,实参列表之前。显式模板实参按从左至右的顺序与对应的模版参数匹配‘第一个模版实参与第一个模版参数匹配,第二个与第二个匹配,以此类推。只有尾部(最右)参数的显式模版实参才可以忽略,而且前提是他们可以从函数参数推断出来。

对于尾置返回类型:由于尾置返回出现在参数列表之后,他可以使用函数的参数。

当我们用一个函数模版初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模版实参。如果不能从函数指针类型确定模版实参,则产生错误。

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

C++语音在正常绑定规则之外定义了两个例外规则,这两个例外规则是move这种标准库设施正确工作的基础。

第一个例外规则影响右值引用参数的推断如果进行。当我们将一个左值(如 i )传递给函数的右值引用参数,且此右值引用指向模版类型参数(如T&&)时,编译器推断模版类型参数为实参的左值引用类型。通常我们不能(直接)定义一个引用的引用,但是,通过类型别名或通过模版类型参数间接定义是可以的。

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

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

std::move可以获得一个绑定到左值上的右值引用。

从一个左值static_cast到一个右值引用是合法的。

转发:某些函数需要将其一个或多个实参连同类型不变地转发给其他函数。在此情况下,我们需要保持被转发实参的所有性质,包括实参类型是否是const的以及实参是做事还是右值。通过将一个函数定义为一个指向模版类型参数的右值引用,我们可以保持其对应实参的所有类型信息。而使用引用参数(无论是左值还是右值)使得我们可以保持const属性,因为在引用类型中的const是底层的。。即如果一个函数参数是指向模版类型参数的右值引用,它对应的实参的const属性和右值/左值属性将得到保持。

std::forward必须通过显式模版实参来调用,forward返回该显式实参类型的右值引用。即forward<T>的返回类型是T&&。

当用于一个指向模版参数类型的右值引用函数参数(T&&)时,forward会保持实参类型的所有细节。和std::move一样不适用using声明是一个好主意。

重载与模版:

函数模版可以被另一个模版或者一个普通非模版函数重载。

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

1、对于一个调用,其后选函数包括所有模版实参推断成功的函数模版实例 。

2、候选的函数模版总是可行的,因为模版实参推断会排除任何不可行的模版。

3、与往常一样,可行函数(模版非模版)按类型转换(如果对此调用需要的话)来排序、当然,可以用于函数模版调用的转换是非常有限的(只有const转换,数组或函数到指针的转换)。

4、与往常一样,如果恰有一个函数提供比任何其他函数都要好的匹配,则选择此函数。但是,如果有多个函数提供同样好的匹配,则:

——如果同样好的函数中只有一个是非模版函数,则选择此函数;

——如果同样好的函数中没有非模版函数,而有多个函数模版,且其中一个模版比其他模版更特例化,则选择此函数。

——否则调用有歧义。

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

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

可变参数模版:

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

我们用一个省略号来指出一个模版参数或函数参数表示一个包。在一个模版参数列表中,class……或typename……指出接下来的参数表示零个或多个类型的列表;一个类型名后面跟一个

省略号表示零个或多个给定类型的非类型参数的列表。在函数参数列表中,如果一个参数的类型是一个模版参数包,则此参数也是一个函数参数包。例如:

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

声明了foo是一个可变参数函数模版,他有一个名为T的类型参数,和一个名为Args的模版参数包。这个包表示零个或多个额外的类型参数。foo的函数参数列表包含一个const&类型的参数,指向T的类型,还包含一个名为rest的函数参数包,此包表示零个或多个函数参数。

与往常一样,编译器从函数的实参推断模版参数类型。对于一个可变参数模版,编译器还会推断包中参数的数目。

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

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

模版特例化:

编写单一模版,使之对任何可能的模版实参都是最合适的,都能实例化,这并不总是能办到。造某些情况下,通用模板的定义对特定类型是不合适的:通用定义可能编译失败或做的不正确。其他时候,我们也可以利用某些特定知识来编写更高效的代码,而不是从通用模版实例化。当我们不能(或不希望)使用模版版本时,可以定义类或模版函数的一个特例化版本。

一个特例化版本就是模版的一个独立的定义,在其中一个或多个模版参数被指定为特定的版本。

当我们特例化一个函数模版时,必须为原模版中的每个模版参数都提供实参。为了指出我们正在实例化一个模版,应使用关键字template后跟一个空尖括号(<>)。空尖括号指出我们将为原模版的所有模版参数提供实参。当我们定义一个特例化版本时,函数参数类型必须与一个先前声明的模版中对应的类型匹配。

当定义函数模板的特例化版本时,我们本质上接管了编译器的工作。即,我们为原模版的一个特殊实例提供了定义。重要的是要弄清:一个特例化版本本质上是一个实例,而非函数名的一个重载版本。因此,特例化不影响函数匹配。

普通作用域规则应用与特例化:为了特例化一个模版,原模版的声明必须在作用域中。而且,在任何使用模板实例的代码之前,特例化版本的声明也必须在作用域中。对于普通类和函数,丢失声明的情况(通常)很容易发现——编译器将不能继续处理我们的代码。但是,如果丢失了一个特例化版本的声明,编译器通常可以用原模版生成代码。由于在丢失特例化版本时编译器通常会实例化原模版,很容易产生模版及其特例化版本声明顺序导致的错误,而这种错误又很难查找。如果一个程序使用了一个特例化版本,而同时原模版的一个实例具有相同的模版参数集合,就会产生错误。但是这种错误编译器又无法发现。因此,模版及其特例化版本应该声明在同一个头文件中。所有同名模版的声明应该放在前面,然后是这些模版的特例化版本。

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

由于一个部分特例化本质是一个模版,与往常一样,我们首先定义模版参数。类似任何其他特例化版本,部分特例化版本的名字与原模版的名字。对每个为完全确定类型的模版参数,在特例化版本的模版参数列表中都有一项与之对应。在类名之后,我们为要特例化的模版参数指定实参,这些实参列于模版名之后的尖括号中。这些实参于原始模版中的参数按位置对应。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lucky登

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值