欢迎访问中国建设银行网上银行网站,广州三合一网站建设,新品发布会主持人台词,宜宾市网站建设函数模板 现在的C编译器实现了C新增的一项特性—函数模板。函数模板是通用的函数描述#xff0c;也就是说#xff0c; 它们使用泛型来定义函数#xff0c;其中的泛型可用具体的类型#xff08;如int 或double#xff09;替换。通过将类型作为参数传 递给模板#xff0c;…函数模板现在的C编译器实现了C新增的一项特性—函数模板。函数模板是通用的函数描述也就是说它们使用泛型来定义函数其中的泛型可用具体的类型如int 或double替换。通过将类型作为参数传递给模板可使编译器生成该类型的函数。由于模板允许以泛型而不是具体类型的方式编写程序因此有时也被称为通用编程。由于类型是用参数表示的因此模板特性有时也被称为参数化类型parameterized types。下面介绍为何需要这种特性以及其工作原理。在前面的程序清单8.4 中定义了一个交换两个int 值的函数。假设要交换两个double 值则一种方法是复制原来的代码并用double 替换所有的int。如果需要交换两个char 值可以再次使用同样的技术。进行这种修改将浪费宝贵的时间且容易出错。如果进行手工修改则可能会漏掉一个int。如果进行全局查找和替换如用double 替换int时可能将int x; short interval;转换为double x; short doubleerval;C的函数模板功能能自动完成这一过程可以节省时间而且更可靠。函数模板允许以任意类型的方式来定义函数。例如可以这样建立一个交换模板templatetypename Anytype void Swap(AnyType a,AnyType b) { AnyType temp; tempa; ab; btemp; }typename 关键字使得参数AnyType 表示类型这一点更为明显然而有大量代码库是使用关键字class开发的。在这种上下文中这两个关键字是等价的。本书使用了这两种形式旨在让您在其他地方遇到它们时不会感到陌生。提示如果需要多个将同一种算法用于不同类型的函数请使用模板。如果不考虑向后兼容的问题并愿意键入较长的单词则声明类型参数时应使用关键字typename 而不使用class。要让编译器知道程序需要一个特定形式的交换函数只需在程序中使用Swap( )函数即可。编译器将检查所使用的参数类型并生成相应的函数。程序清单8.11 演示为何可以这样做。该程序的布局和使用常规函数时相同在文件的开始位置提供模板函数的原型并在main( )后面提供模板函数的定义。这个示例采用了更常见的做法即将T 而不是AnyType 用作类型参数。#include iostream templatetypename T void Swap(T a, T b); int main() { using namespace std; int i 10; int j 20; cout i,j i , j .\n; cout Using compiler-generated int swapper:\n; Swap(i, j); cout Now i,j i , j .\n; double x 24.5; double y 81.7; cout x,y x , y .\n; cout Using compiler-generated double swapper:\n; Swap(x, y); cout Now x,y x , y .\n; return 0; } templatetypename T void Swap(T a, T b) { T temp; temp a; a b; b temp; }需要多个对不同类型使用同一种算法的函数时可使用模板如程序清单8.11 所示。然而并非所有的类型都使用相同的算法。为满足这种需求可以像重载常规函数定义那样重载模板定义。和常规重载一样被重载的模板的函数特征标必须不同。例如程序清单8.12 新增了一个交换模板用于交换两个数组中的元素。原来的模板的特征标为(T , T )而新模板的特征标为(T [ ], T [ ], int)。注意在后一个模板中最后一个参数的类型为具体类型int而不是泛型。并非所有的模板参数都必须是模板参数类型。编译器见到twotemps.cpp 中第一个Swap( )函数调用时发现它有两个int 参数因此将它与原来的模板匹配。但第二次调用将两个int 数组和一个int 值用作参数这与新模板匹配。#include iostream templatetypename T void Swap(T a, T b); templatetypename T void Swap(T* a, T* b, int n); void Show(int a[]); const int Lim 8; int main() { using namespace std; int i 10, j 20; cout i,j i , j .\n; cout Using compiler-generated in swapper :\n; Swap(i, j); cout Now i,j i , j .\n; int d1[Lim] { 0,7,0,4,1,7,7,6 }; int d2[Lim] { 0,7,2,0,1,8,6,9 }; cout Original arrays:\n; Show(d1); Show(d2); Swap(d1, d2, Lim); cout Swapped arrays:\n; Show(d1); Show(d2); return 0; } templatetypename T void Swap(T a, T b) { T temp; tempa; a b; b temp; } templatetypename T void Swap(T a[], T b[], int n) { T temp; for (int i 0; i n; i) { temp a[i]; a[i] b[i]; b[i] temp; } } void Show(int a[]) { using namespace std; cout a[0] a[1] /; cout a[2] a[3] /; for (int i 4; i Lim; i) cout a[i]; cout endl; }模板的局限性假设有如下模板函数templateclass T void f(T a,T b) { ... }通常代码假定可执行哪些操作。例如下面的代码假定定义了赋值但如果T 为数组这种假设将不成立ab;同样下面的语句假设定义了但如果T 为结构该假设便不成立if(ab)另外为数组名定义了运算符但由于数组名为地址因此它比较的是数组的地址而这可能不是您希望的。下面的语句假定为类型T 定义了乘法运算符但如果T 为数组、指针或结构这种假设便不成立T ca*b;总之编写的模板函数很可能无法处理某些类型。另一方面有时候通用化是有意义的但C语法不允许这样做。例如将两个包含位置坐标的结构相加是有意义的虽然没有为结构定义运算符。一种解决方案是C允许您重载运算符以便能够将其用于特定的结构或类运算符重载将在第11 章讨论。这样使用运算符的模板便可处理重载了运算符的结构。另一种解决方案是为特定类型提供具体化的模板定义下面就来介绍这种解决方案。显式具体化假设定义了如下结构struct job { char name[40] double salary; int floor; };另外假设希望能够交换两个这种结构的内容。原来的模板使用下面的代码来完成交换tempa ab; btemp;由于C允许将一个结构赋给另一个结构因此即使T 是一个job 结构上述代码也适用。然而假设只想交换salary 和floor 成员而不交换name 成员则需要使用不同的代码但Swap( )的参数将保持不变两个job 结构的引用因此无法使用模板重载来提供其他的代码。然而可以提供一个具体化函数定义—称为显式具体化explicit specialization其中包含所需的代码。当编译器找到与函数调用匹配的具体化定义时将使用该定义而不再寻找模板。具体化机制随着C的演变而不断变化。下面介绍C标准定义的形式。1第三代具体化ISO/ANSI C标准试验其他具体化方法后C98 标准选择了下面的方法。对于给定的函数名可以有非模板函数、模板函数和显式具体化模板函数以及它们的重载版本。显式具体化的原型和定义应以template打头并通过名称来指出类型。具体化优先于常规模板而非模板函数优先于具体化和常规模板。下面是用于交换job 结构的非模板函数、模板函数和具体化的原型void Swap(job ,job ); templatetypename T void Swap(T ,T ); templatevoid Swapjob(job ,job );正如前面指出的如果有多个原型则编译器在选择原型时非模板版本优先于显式具体化和模板版本而显式具体化优先于使用模板生成的版本。例如在下面的代码中第一次调用Swap( )时使用通用版本而第二次调用使用基于job 类型的显式具体化版本。templateclass T void SwapT T ); templatevoid Swapjob(job ,job ); int main() { double u,v; Swap(u,v); job a,b; Swap(a,b); }Swap中的是可选的因为函数的参数类型表明这是job 的一个具体化。因此该原型也可以这样编写template void Swap(job ,job );下面来看一看显式具体化的工作方式。2显式具体化示例程序清单8.13 演示了显式具体化的工作方式。#include iostream templatetypename T void Swap(T a, T b); struct job { char name[40]; double salary; int floor; }; templatevoid Swapjob(job j1, job j2); void Show(job j); int main() { using namespace std; cout.precision(3); cout.setf(ios::fixed, ios::floatfield); int i 10, j 20; cout i,j i , j .\n; cout Using compiler-generated int swappper:\n; Swap(i, j); cout Now i,j i , j .\n; job sue {Susan Yaffe, 7300.60, 7}; job sidney { Sideny Taffe,78060.72,9 }; cout Before job swapping:\n; Show(sue); Show(sidney); Swap(sue, sidney); cout After job swapping:\n; Show(sue); Swap(sue, sidney); cout After job swapping:\n; Show(sue); Show(sidney); return 0; } templatetypename T void Swap(T a, T b) { T temp; temp a; a b; b temp; } templatevoid Swapjob(job j1, job j2) { double t1; int t2; t1 j1.salary; j1.salary j2.salary; j2.salary t1; t2 j1.floor; j1.floor j2.floor; j2.floor t2; } void Show(job j) { using namespace std; cout j.name : $ j.salary on floor j.floor endl; }实例化和具体化为进一步了解模板必须理解术语实例化和具体化。记住在代码中包含函数模板本身并不会生成函数定义它只是一个用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时得到的是模板实例instantiation。例如在程序清单8.13 中函数调用Swap(i, j)导致编译器生成Swap( )的一个实例该实例使用int 类型。模板并非函数定义但使用int 的模板实例是函数定义。这种实例化方式被称为隐式实例化implicit instantiation因为编译器之所以知道需要进行定义是由于程序调用Swap( )函数时提供了int 参数。最初编译器只能通过隐式实例化来使用模板生成函数定义但现在C还允许显式实例化explicitinstantiation。这意味着可以直接命令编译器创建特定的实例如Swap( )。其语法是声明所需的种类—用符号指示类型并在声明前加上关键字templatetemplate void Swapint(int,int);实现了这种特性的编译器看到上述声明后将使用Swap( )模板生成一个使用int 类型的实例。也就是说该声明的意思是“使用Swap( )模板生成int 类型的函数定义。”与显式实例化不同的是显式具体化使用下面两个等价的声明之一templatevoid Swapint(int ,int ); templatevoid Swapint ,int ;区别在于这些声明的意思是“不要使用Swap( )模板来生成函数定义而应使用专门为int 类型显式地定义的函数定义”。这些原型必须有自己的函数定义。显式具体化声明在关键字template 后包含而显式实例化没有。试图在同一个文件或转换单元中使用同一种类型的显式实例和显式具体化将出错。还可通过在程序中使用函数来创建显式实例化。例如请看下面的代码templateclass T T Add(T a,T b) { return ab; } int m6; double x10.2; coutAdddouble(x,m)endl;这里的模板与函数调用Add(x, m)不匹配因为该模板要求两个函数参数的类型相同。但通过使用Add(x, m)可强制为double 类型实例化并将参数m 强制转换为double 类型以便与函数Add(double, double)的第二个参数匹配。如果对Swap()做类似的处理结果将如何呢int m5; double x14.3; Swapdouble(m,x);这将为类型double 生成一个显式实例化。不幸的是这些代码不管用因为第一个形参的类型为double不能指向int 变量m。隐式实例化、显式实例化和显式具体化统称为具体化specialization。它们的相同之处在于它们表示的都是使用具体类型的函数定义而不是通用描述。引入显式实例化后必须使用新的语法—在声明中使用前缀template 和template 以区分显式实例化和显式具体化。通常功能越多语法规则也越多。下面的代码片段总结了这些概念templateclass T void Swap(T ,T ); templatevoid Swapjob ,job ); int main(void) { template void Swapchar(char ,char ); short a,b; Swap(a,b); job n,m; Swap(n,m); char g,h; Swap(g,h); }编译器看到char 的显式实例化后将使用模板定义来生成Swap( )的char 版本。对于其他Swap( )调用编译器根据函数调用中实际使用的参数生成相应的版本。例如当编译器看到函数调用Swap(a, b)后将生成Swap( )的short 版本因为两个参数的类型都是short。当编译器看到Swap(n, m)后将使用为job 类型提供的独立定义显式具体化。当编译器看到Swap(g, h)后将使用处理显式实例化时生成的模板具体化。编译器选择使用哪个函数版本对于函数重载、函数模板和函数模板重载C需要且有一个定义良好的策略来决定为函数调用使用哪一个函数定义尤其是有多个参数时。这个过程称为重载解析overloading resolution。详细解释这个策略将需要将近一章的篇幅因此我们先大致了解一下这个过程是如何进行的。第1 步创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数。第2 步使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数为此有一个隐式转换序列其中包括实参类型与相应的形参类型完全匹配的情况。例如使用float 参数的函数调用可以将该参数转换为double从而与double 形参匹配而模板可以为float 生成一个实例。第3 步确定是否有最佳的可行函数。如果有则使用它否则该函数调用出错。考虑只有一个函数参数的情况如下面的调用may(B);首先编译器将寻找候选者即名称为may( )的函数和函数模板。然后寻找那些可以用一个参数调用的函数。例如下面的函数符合要求因为其名称与被调用的函数相同且可只给它们传递一个参数void may(int); float may(float,float 3); void may(char); char * may(const char *); char may(const char ) templateclass Tvoid may(const T ); templateclass Tvoid may(T *);****注意只考虑特征标而不考虑返回类型。其中的两个候选函数#4 和#7不可行因为整数类型不能被隐式地转换即没有显式强制类型转换为指针类型。剩余的一个模板可用来生成具体化其中T 被替换为char 类型。这样剩下5 个可行的函数其中的每一个函数如果它是声明的唯一一个函数都可以被使用。接下来编译器必须确定哪个可行函数是最佳的。它查看为使函数调用参数与可行的候选函数的参数匹配所需要进行的转换。通常从最佳到最差的顺序如下所述。 - 1完全匹配但常规函数优先于模板。2提升转换例如char 和shorts 自动转换为intfloat 自动转换为double。3标准转换例如int 转换为charlong 转换为double。4用户定义的转换如类声明中定义的转换。例如函数#1 优于函数#2因为char 到int 的转换是提升转换参见第3 章而char 到float 的转换是标准转换参见第3 章。函数#3、函数#5 和函数#6 都优于函数#1 和#2因为它们都是完全匹配的。#3和#5 优于#6因为#6 函数是模板。这种分析引出了两个问题。什么是完全匹配如果两个函数如#3 和#5都完全匹配将如何办呢通常有两个函数完全匹配是一种错误但这一规则有两个例外。显然我们需要对这一点做更深入的探讨。1完全匹配和最佳匹配进行完全匹配时C允许某些“无关紧要的转换”。表8.1 列出了这些转换—Type 表示任意类型。例如int 实参与int 形参完全匹配。注意Type 可以是char 这样的类型因此这些规则包括从char 到const char 的转换。Typeargument-list意味着用作实参的函数名与用作形参的函数指针只要返回类型和参数列表相同就是匹配的第7 章介绍了函数指针以及为何可以将函数名作为参数传递给接受函数指针的函数。第9 章将介绍关键字volatile。假设有下面的函数代码struct blot{int a;char b[10]}; blot ink{25,spots}; recycle(ink);在这种情况下下面的原型都是完全匹配的void recycle(blot); void recycle(const blot); void recyle(blot ); void recycle(const blot );正如您预期的如果有多个匹配的原型则编译器将无法完成重载解析过程如果没有最佳的可行函数则编译器将生成一条错误消息该消息可能会使用诸如“ambiguous二义性”这样的词语。然而有时候即使两个函数都完全匹配仍可完成重载解析。首先指向非const 数据的指针和引用优先与非const 指针和引用参数匹配。也就是说在recycle( )示例中如果只定义了函数#3 和#4 是完全匹配的则将选择#3因为ink 没有被声明为const。然而const 和非const 之间的区别只适用于指针和引用指向的数据。也就是说如果只定义了#1 和#2则将出现二义性错误。一个完全匹配优于另一个的另一种情况是其中一个是非模板函数而另一个不是。在这种情况下非模板函数将优先于模板函数包括显式具体化。如果两个完全匹配的函数都是模板函数则较具体的模板函数优先。例如这意味着显式具体化将优于使用模板隐式生成的具体化struct blot{int a;char b[10]}; templateclass Typevoid recycle(Type t); templatevoid recyclebloat(blot t); blot ink{25,spots}; recycle(ink);术语“最具体most specialized”并不一定意味着显式具体化而是指编译器推断使用哪种类型时执行的转换最少。例如请看下面两个模板templateclass Typevoid recycle(Type t); templateclass Typevoid recycle(Type * t);假设包含这些模板的程序也包含如下代码struct blot{int a;char b[10]}; blot ink{25,spots}; recycle(ink);recycle(ink)调用与#1 模板匹配匹配时将Type 解释为blot *。recycleink函数调用也与#2 模板匹配这次Type 被解释为ink。因此将两个隐式实例—recycleblot *(blot *)和recycle (blot *)发送到可行函数池中。在这两个模板函数中recycleblot *(blot *)被认为是更具体的因为在生成过程中它需要进行的转换更少。也就是说#2 模板已经显式指出函数参数是指向Type 的指针因此可以直接用blot 标识Type而#1 模板将Type 作为函数参数因此Type 必须被解释为指向blot 的指针。也就是说在#2 模板中Type已经被具体化为指针因此说它“更具体”。用于找出最具体的模板的规则被称为函数模板的部分排序规则partial ordering rules。和显式实例一样这也是C98 新增的特性。2部分排序规则示例我们先看一个完整的程序它使用部分排序规则来确定要使用哪个模板定义。程序清单8.14 有两个用来显示数组内容的模板定义。第一个定义模板A假设作为参数传递的数组中包含了要显示的数据第二个定义模板B假设数组元素为指针指向要显示的数据。简而言之重载解析将寻找最匹配的函数。如果只存在一个这样的函数则选择它如果存在多个这样的函数但其中只有一个是非模板函数则选择该函数如果存在多个适合的函数且它们都为模板函数但其中有一个函数比其他函数更具体则选择该函数。如果有多个同样合适的非模板函数或模板函数但没有一个函数比其他函数更具体则函数调用将是不确定的因此是错误的当然如果不存在匹配的函数则也是错误。#include iostream templatetypename T void ShowArray(T arr[], int n); templatetypename T void ShowArray(T* arr[], int n); struct debts { char name[50]; double amount; }; int main() { using namespace std; int things[6] { 13,31,103,301,310,120 }; struct debts mr_E[3] { {Ima Wolfe,2400.0}, {Ura Foxe,1300.0}, {Iby Stout,1800.0} }; double* pd[3]; for (int i 0; i 3; i) pd[i] mr_E[i].amount; cout Listing Mr.Es counts of things:\n; ShowArray(things, 6); cout Listing Mr.Es debts:\n; ShowArray(pd, 3); return 0; } templatetypename T void ShowArray(T arr[], int n) { using namespace std; cout template A\n; for (int i 0; i n; i) cout arr[i] ; cout endl; } templatetypename T void ShowArray(T* arr[], int n) { using namespace std; cout template E\n; for (int i 0; i n; i) cout *arr[i] ; cout endl; }3自己选择在有些情况下可通过编写合适的函数调用引导编译器做出您希望的选择。请看程序清单8.15该程序将模板函数定义放在文件开头从而无需提供模板原型。与常规函数一样通过在使用函数前提供模板函数定义它让它也充当原型。#include iostream templateclass T T lesser(T a, T b) { return a b ? a : b; } int lesser(int a, int b) { a a 0 ? -a : a; b b 0 ? -b : b; return a b ? a : b; } int main() { using namespace std; int m 20; int n -30; double x 15.5; double y 25.9; cout lesser(m, n) endl; cout lesser(x, y) endl; cout lesser(m, n) endl; cout lesserint(x, y) endl; return 0; }4多个参数的函数将有多个参数的函数调用与有多个参数的原型进行匹配时情况将非常复杂。编译器必须考虑所有参数的匹配情况。如果找到比其他可行函数都合适的函数则选择该函数。一个函数要比其他函数都合适其所有参数的匹配程度都必须不比其他函数差同时至少有一个参数的匹配程度比其他函数都高。本书并不是要解释复杂示例的匹配过程这些规则只是为了让任何一组函数原型和模板都存在确定的结果。模板函数的发展在C发展的早期大多数人都没有想到模板函数和模板类会有这么强大而有用它们甚至没有就这个主题发挥想象力。但聪明而专注的程序员挑战模板技术的极限阐述了各种可能性。根据熟悉模板的程序员提供的反馈C98 标准做了相应的修改并添加了标准模板库。从此以后模板程序员在不断探索各种可能性并消除模板的局限性。C11 标准根据这些程序员的反馈做了相应的修改。下面介绍一些相关的问题及其解决方案。1是什么类型在C98 中编写模板函数时一个问题是并非总能知道应在声明中使用哪种类型。请看下面这个不完整的示例templateclass T1,class T2 void ft(T1 x,T2 y) { ?type?xpyxpyy; }xpy 应为什么类型呢由于不知道ft()将如何使用因此无法预先知道这一点。正确的类型可能是T1、T2 或其他类型。例如T1 可能是double而T2 可能是int在这种情况下两个变量的和将为double 类型。T1 可能是short而T2 可能是int在这种情况下两个变量的和为int 类型。T1 还可能是short而T2 可能是char在这种情况下加法运算将导致自动整型提升因此结果类型为int。另外结构和类可能重载运算符这导致问题更加复杂。因此在C98 中没有办法声明xpy 的类型。2关键字decltypeC11C11 新增的关键字decltype 提供了解决方案。可这样使用该关键字int x; decltype(x)y;给decltype 提供的参数可以是表达式因此在前面的模板函数ft()中可使用下面的代码decltype(xy)xpy; xpyxy;另一种方法是将这两条语句合而为一decltype(xy)xpyxy;因此可以这样修复前面的模板函数ft()templateclass T1,class T2 void fit(T1 x,T2 y) { decltype(xy)xpyxy; }decltype 比这些示例演示的要复杂些。为确定类型编译器必须遍历一个核对表。假设有如下声明decltype(expression)var;则核对表的简化版如下第一步如果expression 是一个没有用括号括起的标识符则var 的类型与该标识符的类型相同包括const 等限定符double x5.5; double y7.9; double rxx; const double *pd; decltype(x)w; decltype(rx)uy; decltype(pd)v;第二步如果expression 是一个函数调用则var 的类型与函数的返回类型相同long indeed(int); decltype(indeed(3))m;注意并不会实际调用函数。编译器通过查看函数的原型来获悉返回类型而无需实际调用函数。第三步如果expression 是一个左值则var 为指向其类型的引用。这好像意味着前面的w 应为引用类型因为x 是一个左值。但别忘了这种情况已经在第一步处理过了。要进入第三步expression 不能是未用括号括起的标识符。那么expression 是什么时将进入第三步呢一种显而易见的情况是expression 是用括号括起的标识符double xx4.4; decltype((xx))r1xx; decltype(xx)wxx;顺便说一句括号并不会改变表达式的值和左值性。例如下面两条语句等效xx98.6; (xx)98.6;第四步如果前面的条件都不满足则var 的类型与expression 的类型相同int j3; int kj; int nj; decltype(j6)i1; decltype(100L)i2; decltype(kn)i3;请注意虽然k 和n 都是引用但表达式kn 不是引用它是两个int 的和因此类型为int。如果需要多次声明可结合使用typedef 和decltypetemplateclass T1,class T2 void fit(T1 x,T2 y) { ... typedef decltype(xy)xytype; xytype xpyxpyy; xytype arr[10]; xytype rxyarr[2]; }3另一种函数声明语法C11 后置返回类型有一个相关的问题是decltype 本身无法解决的。请看下面这个不完整的模板函数templateclass T1,class T2 ?typeof?gt(T1 x,T2 y) { ... return xy; }同样无法预先知道将x 和y 相加得到的类型。好像可以将返回类型设置为decltype ( x y)但不幸的是此时还未声明参数x 和y它们不在作用域内编译器看不到它们也无法使用它们。必须在声明参数后使用decltype。为此C新增了一种声明和定义函数的语法。下面使用内置类型来说明这种语法的工作原理。对于下面的原型double h(int x,float y);使用新增的语法可编写成这样auto h(int x,float y)-double;这将返回类型移到了参数声明后面。-double 被称为后置返回类型trailing return type。其中auto 是一个占位符表示后置返回类型提供的类型这是C11 给auto 新增的一种角色。这种语法也可用于函数定义auto h(int x,float y)-double通过结合使用这种语法和decltype便可给gt()指定返回类型如下所示templateclass T1,class T2 auto gt(T1 x,T2 y)-decltype(xy) { return xy; }现在decltype 在参数声明后面因此x 和y 位于作用域内可以使用它们。