分类: C/C++
2013-04-29 18:04:24
原文地址:第十六章 模板与泛型编程 作者:zero_coder
1. 所谓泛型程序设计就是以独立于任何特定类型方式编写的代码.使用泛型程序时,我们需要提供具体程序实例所做的类型和值.并且依赖于某种形式的多态性.
面向对象程序设计中的多态性在运行时应用于存在继承关系的类,我们能够编写使用这些类的代码,忽略基类和派生类之间类型上的差异
另外面向对象程序设计所依赖的多态性称为运行时的多态性,泛型程序设计所依赖的多态性称为编译时多态性或者参数式多态性.
2. 函数模版:独立于类型的函数,可作为一种方式,产生函数的特定类型版本.
定义格式为:
等价于
函数模版可以用于非模版函数一样的方式声明为inline.说明符放在模版形参表之后,返回类型之前,不能放在关键词template之前
3. 模版形参的语法:
i. 模版形参的名字可以在声明为模版形参之后直到模板声明或定义的末尾处使用.
ii. 用作模板形参的名字不能在范本内部重用.这一限制意味着模版形参的名字只能在同一模板形参表中使用一次
iii. 向其他任意函数或者类一样,对于模板可以只声明而不定义.声明必须指出函数或类是一个范本.
4. 在模板定义内部指定类型:
通过在成员名前加关键词typename,告诉编译程序将成员当作类型.如果不加,则编译程序假定这样的名字指定数据成员而非类型.
如果拿不准是否需要以typename指明一个名字是一个类型,那么指定它是一个好主意.
5. 模板非类型形参是模板定义内部的常量值.在需要常量表达式的时候,可以使用非类型形参指定.
在函数模板内部完成的操作限制了可用于实例化该函数的类型.程序员的责任是,保证用作函数实参的类型实际上支持所用的任意操作,以及保证在模板使用那些操作的环境中那些操作运行正常.
我们在编写模板代码时,对实参类型的要求尽可能少是很有益的.
6. 编写泛型代码的两个重要的原则:
a) 范本的形参是const引用
b) 函数体中的测试只有<比较
一般而言,编译范本时,编译程序可能会在三个阶段中标识错误:
◆ 第一阶段是编译模板定义本身.在这阶段中编译程序一般不能那个发现许多错误,可以检测到诸如漏掉分好或者变量名拼写错误一类的语法错误
◆ 第二个错误检测事件是在编译程序见到模板使用时,在这个阶段,编译程序仍然没有很多检查可做.只检查实参的数目和类型是否恰当
◆ 产生第三个时间是在实例化的时候,对程序是否有效所知不多.类似的甚至可能会在已经成功编译了使用模板的每个文件之后出现编译错误.只在实例化期间检测错误的情况很少,错误检测可能发生在连结时.
7. 类模板的每次实例化都会产生一个独立的类类型.为int类型实例化的函数(或类)与其他的函数(或类)没有关系,对其他函数(或类)也没有特殊的访问权.
8. 范本实参推断:
◆ 多个类型形参的实参必须完全匹配
◆ 类型形参的实参的受限转换:
一般而言,不会转换实参以匹配已有的实例化,相反,会产生新的实例.除了产生新的实例化之外,编译器只会执行一下两种转换:
■ const转换:接受const引用或者const指针的函数可以分别用非const对象的引用或指针来调用,无需产生实例化.如果函数接受非引用类型,形参类型和实参都忽略const,即无论传递const或非const对象给接受非引用类型的函数,都使用相同的实例化
■ 数组或函数到指针的转换:如果模板形参不是引用类型,则对数组或函数类型的实参应用常规指针转换.数组实参将当作指向其第一个元素的指针,函数实参当作指向函数类型的指针.
■ 应用于非模板实参的常规转换:类型转换的限制只适用于类相关为模板形参的那些实参.
■ 模板实参推断与函数指针:可以使用函数模板对函数指针进行初始化或者赋值,这样做的时候,编译器使用指针的类型实例化具有适当模板实参的模板版本.
获取函数模板实例化的地址的时候,上下文必须是这样的:它允许为每个模板形参确定唯一的类型或值.
9. 函数模板的显示实参:
◆ 指定显式模板实参:
◆ 在返回类型中使用类型形参,指定返回类型的一种方式是引入第三个模板形参,他必须由调用者显示指定.
只是要注意一个问题:没有实参的类型可用于推断Type的类型,相反,调用者必须在每次调用fun时为该形参显式提供实参.
显式模板实参从左至右与对应模板形参相匹配.假如可以从函数形参推断,则只有结尾形参的显式模板实参可以省略.这类似于重载函数的调用规则.
这个例子中,初始化了Type的类型,但是由于Type2使用的类型是必须显式给出的,所以我们不能只是在调用时使用而是只能全部形参都要显式指定实参.
◆ 显示实参与函数模板的指针:通过此来消除二义性.
10. 当编译器看到模板定义的时候,它不立即产生代码.只有在看到用到模板时,如调用了函数目标那或者定义了类模板对象的时候,编译器才产生与特定类型的模板实参.
模板编译的特例:要进行实例化,编译器必须能够访问定义模板的源代码.当调用函数模板或者类模板的成员函数的时候,编译器需要函数定义,需要那些通常放在源文件中的代码.
11. C++中为编译模板代码定义的两种模型:
■ 包含编译模型:编译时,编译器必须看到用到的所有模板的定义.一般可以将所使用到的头文件,模板定义放在条件编译里面.
■ 分别编译模型:编译器会为我们跟踪相关的模版定义.但是我们必须让编译器知道要记住给定的模版定义,可以使用export关键字来做这件事.
在类的实现文件中使用export.导出类的成员将自动声明为导出的.(这与DLL类似)
12. 模版中包含两种名字:独立于模板形参和依赖于模板形参的那些名字.
设计者的责任是,保证所有不依赖于模板形参的名字在模板本身的作用域中定义.
模板用户的责任是,保证与用来实例化模板的类型相关的所有函数、类型和操作符的声明可见的.在实例化模板的成员或函数模板的时候,用户必须保证这些声明是可见的.
13. 通常使用类模板的名字的时候,必须指定模板形参.这一规则也有一个例外:在类本身的作用域中,可以使用类模板的非限定名.
类模板成员函数的定义具有下面的形式:
▲ 必须以关键字template开头,后接类的模板形参表
▲ 必须指出它是哪个类的成员
▲ 类名必须包含其模板形参
14. 类模板成员函数与其他模板函数的不同之处:在实例化类模板成员函数的时候,编译器不执行模板实参推断,相反,类模板成员函数的模板形参由调用该函数的对象的类型确定.(说白了就是在定义对象的时候就已经定义了成员函数的实参).另外类模板的成员函数只有为程序所用才进行实例化.如果某函数从未使用,则不会实例化该成员函数.
非类型模板实参必须是编译时常量表达式.
15. 在模板类中可以出现三种友元声明,每一种都声明了与一个或多个实体的友元关系:
■ 普通非模板类或者函数的友元声明,将友元关系授予明确指定的类或函数
■ 类模板或者函数模板的友元声明,将授予友元所有实例的访问权
■ 只属于对类模板或者函数模板的特定实例的访问权的友元声明
需要注意的是:当授予对给定模板的所有实例的访问权的时候,在作用域中不需要存在该类模板或者函数模板的声明.实际上编译器将友元声明也当作类或函数的声明对待.
想要限制对特定实例化的友元关系时,必须在可以用于友元声明之前声明类或函数(可以理解为无论是类模板还是函数模板必须遵循变量的使用规则:先定义后使用).
16. 当在类模板作用域外部定义成员模板的时候,必须包含两个模板形参:类模板形参和自己的模板形参.(注意顺序)
17. 类模板的静态成员:像非模板的成员函数一样使用,定义static成员的时候,必须在类外部出现数据成员的定义.
函数模板的特化是这样一个定义,该定义中一个或者多个模板形参的实际类型或者实际值是指定的,特化的形式如下:
● 关键字template后面接一对空的尖括号(<>);
● 再接模板名和一对尖括号,尖括号中指定这个特化定义的模板形参;
● 函数形参表;
● 函数体.
例子:
//special version of compare to handle C-sytle character strings
template <>
int compare
{
return strcmp(v1,v2);
}
18. 与任意函数一样,函数模板特化可以声明而无须定义.模板特化声明看起来与定义很像,但忽略了函数体.
当定义非模板函数的时候,对实参应用常规转换;当特化模板的时候对实参类型不应用转换.在模板特化版本的调用中,实参类型必须与特化版本函数的形参类型完全匹配,如果不匹配,编译器将为实参从模板定义实例化一个实例.
与其他声明一样,应该在一个头文件中包含模板特化的声明,然后使用该特化的每个源文件包含该头文件.
对于具有同一模板实参集的同一模板,程序不能既有显式特化又有实例化.
19. 类模板特化应该与它所特化的模板定义相同接口,否则当用户试图是有那个未定义的成员时会感到奇怪.在特化外部定义成员时,成员之前不能加template<>标记.
部分特化的定义域通用模板的定义完全不会冲突.部分特化可以具有与通用类模板完全不同的成员集合.类模板成员的通用定义永远不会用来实例化类模板部分特化的成员.
20. 函数匹配与函数模板
如果重载函数中既有普通函数又有函数模板,确定函数的调用的步骤如下:
■ 为这个函数名建立候选函数集合,包括:
● 与被调用函数名字相同的任意普通函数
● 任意函数模版实例化,在其中,模板实参推断发现了与调用中所用函数实参相匹配的模板实参.
■ 确定那些普通函数是可行的.候选集合中的每个模板实例都是可行的,因为模板实参推断保证函数可以被调用.
■ 如果需要转换来进行调用,根据转换的种类排列可行函数,记住,调用模板函数的实例所允许的转换是有限的.
● 如果只有一个候选函数可选,就调用这个函数
● 如果调用有二义性,从可行函数集合中去掉所有函数模板实例
■ 重新排列去掉函数模板实例的可行函数
● 如果只有一个函数可选,就调用这个函数
● 否则具有二义性.
21. 设计既包含函数模板又包含非模板函数的重载函数集合是困难的,因为可能会使函数的用户感到奇怪,定义函数模板特化几乎总是比使用非模板版本更好.
16.1 模板定义
模板和c#范型一样,建立一个通用的类或函数,其参数类型和返回类型不具体指定,用一个虚拟的类型来代表,通过模板化函数或类实现代码在的重用。
定义语法是:
template
返回类型 函数名(模板形参表)
{
函数体
}
或 :
template
返回类型 函数名(模板形参表)
{
函数体
}
template是一个声明模板的关键字,类型参数一般用T这样的标识符来代表一个虚拟的类型,当使用函数模板时,会将类型参数具体化。typename
和class关键字作用都是用来表示它们之后的参数是一个类型的参数。只不过class是早期C++版本中所使用的,后来为了不与类产生混淆,所以增加个
关键字typename。
函数模板:
template内可以定义多个类型形参,每个形参用,分割并且所有类型前面都要用typename修饰。
template
template
函数模板也可以声明inline 语法是 template
类模板:
和函数模板不一样,类模板无法使用类型推断,所以定义对象时一定要显示传递类型参数。
类型形参名称有自己的作用域:
可以像申明一般函数或类一样声明(而不定义)。但类型形参不能省略 template
模板类型参数可以用typename 或者class 来修饰,大部分情况下二者可以互换。但有一种特殊用方法时需要typename
要注意,这种用法需要满足条件:类型形参T必须要定义内部类inbase 否则会编译错误。
模板编程中还可以在类型形参列表中定义非类型形参,这时非类型形参会被当成常量
范型编程有两个重要原则:形参尽量使用const引用(防止拷贝),形参本身操作尽量少(传递一个不支持函数形参体操作的类型会报错)
16.2 实例化
函数模板可以定义函数指针并予以赋值
16.3 模板编译模型
[1] 当编译器看到模板定义的时候,它不立即产生代码。 只有在看到用到模板时 ,如调用了函数模板或定义了类模板的对象的时候,编译器才产生特定类型的模板实例 。
[2] 一般而言,当调用函数的时候,编译器只需要看到函数的声明。类似地,定义类类型的对象时,类定义必须可用,但成员函数的定义不是必须存在的。因此,应该将类定义和函数声明放在头文件中,而普通函数和类成员函数的定义放在源文件中。
[3] 模板则不同:要进行实例化,编译器必须能够访问定义模板的源代码。 当调用函数模板或类模板的成员函数的时候,编译器需要函数定义,需要哪些通常放在源文件中的代码。
[4] 标准C++为编译模板代码定义了两种模型。 所有编译器都支持第一种模型,称为“包含”模型( inclusion compilation
model) ;只有一些编译器支持第二种模型,“分别编译”模型( separate compilation model) 。
[5] 在两种模型中,构造程序的方式很大程度上是相同的:类定义和函数声明放在头文件中,而函数定义和成员定义放在源文件中。两种模型的不同在于,编译器怎样使用来自源文件的定义 。
[6] 在包含编译模型,编译器必须看到用到的所有模板的定义。一般而言,可以通过在声明函数模板或类模板的头文件中添加一条#include指示使定义可用,该#include引入了包含相关定义的源文件 。
[7] 在分别编译模型中,编译器会为我们跟踪相关的模板定义。但是,我们必须让编译器知道要记住给定的模板定义,可以使用export关键字来做这件事 。export关键字能够指明给定的定义可能会需要在其他文件中产生实例化 。
[8] 在一个程序中,一个模板只能定义为导出一次。 一般我们在函数模板的定义中指明函数模板为导出的
,这是通过在关键字template之前包含export关键字而实现的。对类模板使用export更复杂一些
,记得应该在类的实现文件中使用export,否者如果在头文件中使用了export,则该头文件只能被程序中的一个源文件使用。
[9] 导出类的成员将自动声明为导出的。也可以将类模板的个别成员声明为导出的,在这种情况下,关键字export不在类模板本身指定,而是只在被导出的特定成员定义上指定。任意非导出成员的定义必须像在包含模型中一样对待:定义应放在定义类模板的头文件中。
16.4 类模板成员
普通类不但定义非模板函数成员,也能定义模板函数成员:
可这样调用:
base obj ;
obj.Get
obj.Get("str") ; // 类型推断,等价于obj.Get
如果是模板类
可这样调用:
base
obj.Get
obj.Get("str") ; // 类型推断,等价于obj.Get
类模板或函数模板可以作为其他类的友元,不过由于其特殊性可以做一些限制。
友元函数和模板类情况相似。 第一种友元可以看做是完全申明,第二种和第三种友元则需要至少在base定以前有完全申明,否则会编译错误。
16.5 一个范型句柄类
如果对上一章句柄类有充分理解范型句柄类应该非常容易掌握。
16.6 模板特化
模板的特化(template specialization)分为两类:函数模板的特化和类模板的特化。
函数模板的特化:当函数模板需要对某些类型进行特别处理,称为函数模板的特化。例如:
最后一行比较字符串是否相等。由于对于传入的参数是char *类型的,IsEqual函数模板只是简单的比较了传入参数的值,即两个指针是否相等,因此这里打印0。显然,这与我们的初衷不符。因此,sEqual函 数模板需要对char *类型进行特别处理,即特化:
这样,当IsEqual函数的参数类型为char* 时,就会调用IsEqual特化的版本,而不会再由函数模板实例化。
类模板的特化:与函数模板类似,当类模板内需要对某些类型进行特别处理时,使用类模板的特化。例如:
这里最后一行也是调用模板类compare
不是仅仅比较两个字符指针。因此,需要使用类模板的特化:
注意:进行类模板的特化时,需要特化所有的成员变量及成员函数。