您的位置:首页 > 其它

关于模板类和模板函数的经典文章

2010-03-05 09:34 204 查看
如何组织编写模板程序

前言

常遇到询问使用模板到底是否容易的问题,我的回答是:“模板的使用是容易的,但组织编写却不容易”。看看我们几乎每天都能遇到的模板类吧,如STL,ATL,WTL,以及Boost的模板类,都能体会到这样的滋味:接口简单,操作复杂。

我在5年前开始使用模板,那时我看到了MFC的容器类。直到去年我还没有必要自己编写模板类。可是在我需要自己编写模板类时,我首先遇到的事实却是
“传统”编程方法(在*.h文件声明,在*.cpp文件中定义)不能用于模板。于是我花费一些时间来了解问题所在及其解决方法。

本文对象是那些熟悉模板但还没有很多编写模板经验的程序员。本文只涉及模板类,未涉及模板函数。但论述的原则对于二者是一样的。

问题的产生

通过下例来说明问题。例如在array.h文件中有模板类array:

//array.h

template<typenameT,intSIZE>

classarray

{

Tdata_[SIZE];

array(constarray&other);

constarray&operator=(constarray&other);

public:

array(){};

T&operator[](inti){returndata_[i];}

constT&get_elem(inti)const{returndata_[i];}

voidset_elem(inti,constT&value){data_[i]=value;}

operatorT*(){returndata_;}

};

然后在main.cpp文件中的主函数中使用上述模板:

//main.cpp

#include"array.h"

intmain(void)

{

array<int,50>intArray;

intArray.set_elem(0,2);

intfirstElem=intArray.get_elem(0);

int*begin=intArray;

}

这时编译和运行都是正常的。程序先创建一个含有50个整数的数组,然后设置数组的第一个元素值为2,再读取第一个元素值,最后将指针指向数组起点。

但如果用传统编程方式来编写会发生什么事呢?我们来看看:

将array.h文件分裂成为array.h和array.cpp二个文件(main.cpp保持不变)

//array.h

template<typenameT,intSIZE>

classarray

{

Tdata_[SIZE];

array(constarray&other);

constarray&operator=(constarray&other);

public:

array(){};

T&operator[](inti);

constT&get_elem(inti)const;

voidset_elem(inti,constT&value);

operatorT*();

};

//array.cpp

#include"array.h"

template<typenameT,intSIZE>T&array<T,SIZE>::operator[](inti)

{

returndata_[i];

}

template<typenameT,intSIZE>constT&array<T,SIZE>::get_elem(inti)const

{

returndata_[i];

}

template<typenameT,intSIZE>voidarray<T,SIZE>::set_elem(inti,constT&value)

{

data_[i]=value;

}

template<typenameT,intSIZE>array<T,SIZE>::operatorT*()

{

returndata_;

}

编译时会出现3个错误。问题出来了:

为什么错误都出现在第一个地方?

为什么只有3个链接出错?array.cpp中有4个成员函数。

要回答上面的问题,就要深入了解模板的实例化过程。

模板实例化

程序员在使用模板类时最常犯的错误是将模板类视为某种数据类型。所谓类型参量化(parameterizedtypes)这样的术语导致了这种误解。模板当然不是数据类型,模板就是模板,恰如其名:

编译器使用模板,通过更换模板参数来创建数据类型。这个过程就是模板实例化(Instantiation)。

从模板类创建得到的类型称之为特例(specialization)。

模板实例化取决于编译器能够找到可用代码来创建特例(称之为实例化要素,

pointofinstantiation)。

要创建特例,编译器不但要看到模板的声明,还要看到模板的定义。

模板实例化过程是迟钝的,即只能用函数的定义来实现实例化。

再回头看上面的例子,可以知道array是一个模板,array<int,50>是一个模板实例-
一个类型。从array创建array<int,
50>的过程就是实例化过程。实例化要素体现在main.cpp文件中。如果按照传统方式,编译器在array.h文件中看到了模板的声明,但没有
模板的定义,这样编译器就不能创建类型array<int,
50>。但这时并不出错,因为编译器认为模板定义在其它文件中,就把问题留给链接程序处理。

现在,编译array.cpp时会发生什么问题呢?编译器可以解析模板定义并检查语法,但不能生成成员函数的代码。它无法生成代码,因为要生成代码,需要知道模板参数,即需要一个类型,而不是模板本身。

这样,链接程序在main.cpp或array.cpp中都找不到array<int,50>的定义,于是报出无定义成员的错误。

至此,我们回答了第一个问题。但还有第二个问题,在array.cpp中有4个成员函数,链接器为什么只报了3个错误?回答是:实例化的惰性导致这种现象。在main.cpp中还没有用上operator[],编译器还没有实例化它的定义。

解决方法

认识了问题,就能够解决问题:

在实例化要素中让编译器看到模板定义。

用另外的文件来显式地实例化类型,这样链接器就能看到该类型。

使用export关键字。

前二种方法通常称为包含模式,第三种方法则称为分离模式。

第一种方法意味着在使用模板的转换文件中不但要包含模板声明文件,还要包含模板定义文件。在上例中,就是第一个示例,在array.h中用行内函数
定义了所有的成员函数。或者在main.cpp文件中也包含进array.cpp文件。这样编译器就能看到模板的声明和定义,并由此生成
array<int,50>实例。这样做的缺点是编译文件会变得很大,显然要降低编译和链接速度。

第二种方法,通过显式的模板实例化得到类型。最好将所有的显式实例化过程安放在另外的文件中。在本例中,可以创建一个新文件templateinstantiations.cpp:

//templateinstantiations.cpp

#include"array.cpp"

templateclassarray<int,50>;//显式实例化

array<int,
50>类型不是在main.cpp中产生,而是在templateinstantiations.cpp中产生。这样链接器就能够找到它的定义。用
这种方法,不会产生巨大的头文件,加快编译速度。而且头文件本身也显得更加“干净”和更具有可读性。但这个方法不能得到惰性实例化的好处,即它将显式地生
成所有的成员函数。另外还要维护templateinstantiations.cpp文件。

第三种方法是在模板定义中使用export关键字,剩下的事就让编译器去自行处理了。当我在

Stroustrup的书中读到export
时,感到非常兴奋。但很快就发现VC
6.0不支持它,后来又发现根本没有编译器能够支持这个关键字(第一个支持它的编译器要在2002年底才问世)。自那以后,我阅读了不少关于export
的文章,了解到它几乎不能解决用包含模式能够解决的问题。欲知更多的export关键字,建议读读HerbSutter撰写的文章。

结论

要开发模板库,就要知道模板类不是所谓的"原始类型",要用其它的编程思路。本文目的不是要吓唬那些想进行模板编程的程序员。恰恰相反,是要提醒他们避免犯下开始模板编程时都会出现的错误。

//////////////////////////////

http://www.cnblogs.com/xgchang/archive/2004/11/12/63139.aspx


至是在定义非内联函数时,模板的头文件中也会放置所有的声明和定义。这似乎违背了通常的头文件规则:“不要在分配存储空间前放置任何东西”,这条规则是为
了防止在连接时的多重定义错误。但模板定义很特殊。由template<...>处理的任何东西都意味着编译器在当时不为它分配存储空间,它
一直出于等待状态直到被一个模板实例告知。在编译器和连接器的某一处,有一机制能去掉模板的多重定义,所以为了容易使用,几乎总是在头文件中放置全部的模
板声明和定义。

为什么C++编译器不能支持对模板的分离式编译

刘未鹏(pongba)/文

首先,C++标准中提到,一个编译单元[translation
unit]是指一个.cpp文件以及它所include的所有.h文件,.h文件里的代码将会被扩展到包含它的.cpp文件里,然后编译器编译该.cpp
文件为一个.obj文件,后者拥有PE[Portable
Executable,即windows可执行文件]文件格式,并且本身包含的就已经是二进制码,但是,不一定能够执行,因为并不保证其中一定有main
函数。当编译器将一个工程里的所有.cpp文件以分离的方式编译完毕后,再由连接器(linker)进行连接成为一个.exe文件。

举个例子:

//---------------test.h-------------------//

voidf();//这里声明一个函数f

//---------------test.cpp--------------//

#include”test.h”

voidf()

{

…//dosomething

}//这里实现出test.h中声明的f函数

//---------------main.cpp--------------//

#include”test.h”

intmain()

{

f();//调用f,f具有外部连接类型

}


这个例子中,test.
cpp和main.cpp各被编译成为不同的.obj文件[姑且命名为test.obj和main.obj],在main.cpp中,调用了f函数,然而
当编译器编译main.cpp时,它所仅仅知道的只是main.cpp中所包含的test.h文件中的一个关于void
f();的声明,所以,编译器将这里的f看作外部连接类型,即认为它的函数实现代码在另一个.obj文件中,本例也就是test.obj,也就是
说,main.obj中实际没有关于f函数的哪怕一行二进制代码,而这些代码实际存在于test.cpp所编译成的test.obj中。在
main.obj中对f的调用只会生成一行call指令,像这样:

callf[C++中这个名字当然是经过mangling[处理]过的]


编译时,这个call指令显然是错误的,因为main.obj中并无一行f的实现代码。那怎么办呢?这就是连接器的任务,连接器负责在其它的.obj中
[本例为test.obj]寻找f的实现代码,找到以后将call
f这个指令的调用地址换成实际的f的函数进入点地址。需要注意的是:连接器实际上将工程里的.obj“连接”成了一个.exe文件,而它最关键的任务就是
上面说的,寻找一个外部连接符号在另一个.obj中的地址,然后替换原来的“虚假”地址。

这个过程如果说的更深入就是:

callf这行指令其实并不是这样的,它实际上是所谓的stub,也就是一个

jmp
0x23423[这个地址可能是任意的,然而关键是这个地址上有一行指令来进行真正的call
f动作。也就是说,这个.obj文件里面所有对f的调用都jmp向同一个地址,在后者那儿才真正”call”f。这样做的好处就是连接器修改地址时只要对
后者的call
XXX地址作改动就行了。但是,连接器是如何找到f的实际地址的呢[在本例中这处于test.obj中],因为.obj于.exe的格式都是一样的,在这
样的文件中有一个符号导入表和符号导出表[importtable和export
table]其中将所有符号和它们的地址关联起来。这样连接器只要在test.obj的符号导出表中寻找符号f[当然C++对f作了mangling]的
地址就行了,然后作一些偏移量处理后[因为是将两个.obj文件合并,当然地址会有一定的偏移,这个连接器清楚]写入main.obj中的符号导入表中f
所占有的那一项。

这就是大概的过程。其中关键就是:

编译main.cpp时,编译器不知道f的实现,所有当碰到对它的调用时只是给出一个指示,指示连接器应该为它寻找f的实现体。这也就是说main.obj中没有关于f的任何一行二进制代码。

编译test.cpp时,编译器找到了f的实现。于是乎f的实现[二进制代码]出现在test.obj里。

连接时,连接器在test.obj中找到f的实现代码[二进制]的地址[通过符号导出表]。然后将main.obj中悬而未决的callXXX地址改成f实际的地址。

完成。

然而,对于模板,你知道,模板函数的代码其实并不能直接编译成二进制代码,其中要有一个“具现化”的过程。举个例子:

//----------main.cpp------//

template<classT>

voidf(Tt)

{}

intmain()

{

…//dosomething

f(10);//callf<int>编译器在这里决定给f一个f<int>的具现体

…//dootherthing

}

也就是说,如果你在main.cpp文件中没有调用过f,f也就得不到具现,从而main.obj中也就没有关于f的任意一行二进制代码!!如果你这样调用了:

f(10);//f<int>得以具现化出来

f(10.0);//f<double>得以具现化出来

这样main.obj中也就有了f<int>,f<double>两个函数的二进制代码段。以此类推。

然而具现化要求编译器知道模板的定义,不是吗?

看下面的例子:[将模板和它的实现分离]

//-------------test.h----------------//

template<classT>

classA

{

public:

voidf();//这里只是个声明

};

//---------------test.cpp-------------//

#include”test.h”

template<classT>

voidA<T>::f()//模板的实现,但注意:不是具现

{

…//dosomething

}

//---------------main.cpp---------------//

#include”test.h”

intmain()

{

A<int>a;

a.f();//编译器在这里并不知道A<int>::f的定义,因为它不在test.h里面

//于是编译器只好寄希望于连接器,希望它能够在其他.obj里面找到

//A<int>::f的实现体,在本例中就是test.obj,然而,后者中真有A<int>::f的

//二进制代码吗?NO!!!因为C++标准明确表示,当一个模板不被用到的时

//侯它就不该被具现出来,test.cpp中用到了A<int>::f了吗?没有!!所以实

//际上test.cpp编译出来的test.obj文件中关于A::f的一行二进制代码也没有

//于是连接器就傻眼了,只好给出一个连接错误

//
但是,如果在test.cpp中写一个函数,其中调用A<int>::f,则编译器会将其//具现出来,因为在这个点上[test.cpp
中],编译器知道模板的定义,所以能//够具现化,于是,test.obj的符号导出表中就有了A<int>::f这个符号的地

//址,于是连接器就能够完成任务。

}

关键是:在分离式编译的环境下,编译器编译某一个.cpp文件时并不知道另一个.cpp文件的存在,也不会去查找[当遇到未决符号时它会寄希望于连
接器]。这种模式在没有模板的情况下运行良好,但遇到模板时就傻眼了,因为模板仅在需要的时候才会具现化出来,所以,当编译器只看到模板的声明时,它不能
具现化该模板,只能创建一个具有外部连接的符号并期待连接器能够将符号的地址决议出来。然而当实现该模板的.cpp文件中没有用到模板的具现体时,编译器
懒得去具现,所以,整个工程的.obj中就找不到一行模板具现体的二进制代码,于是连接器也黔

/////////////////////////////////

http://dev.csdn.net/develop/article/19/19587.shtm

C++模板代码的组织方式——包含模式(InclusionModel)选择自sam1111的Blog

关键字TemplateInclusionModel

出处C++Template:TheCompleteGuide

说明:本文译自《C++Template:TheCompleteGuide》一书的第6章中的部分内容。最近看到C++论坛上常有关于模板的包含模式的帖子,联想到自己初学模板时,也为类似的问题困惑过,因此翻译此文,希望对初学者有所帮助。

模板代码有几种不同的组织方式,本文介绍其中最流行的一种方式:包含模式。

链接错误

大多数C/C++程序员向下面这样组织他们的非模板代码:

·类和其他类型全部放在头文件中,这些头文件具有.hpp(或者.H,.h,.hh,.hxx)扩展名。

·对于全局变量和(非内联)函数,只有声明放在头文件中,而定义放在点C文件中,这些文件具有.cpp(或者.C,.c,.cc,.cxx)扩展名。

这种组织方式工作的很好:它使得在编程时可以方便地访问所需的类型定义,并且避免了来自链接器的“变量或函数重复定义”的错误。

由于以上组织方式约定的影响,模板编程新手往往会犯一个同样的错误。下面这一小段程序反映了这种错误。就像对待“普通代码”那样,我们在头文件中定义模板:

//basics/myfirst.hpp

#ifndefMYFIRST_HPP

#defineMYFIRST_HPP

//declarationoftemplate

template<typenameT>

voidprint_typeof(Tconst&);

#endif//MYFIRST_HPP

print_typeof()声明了一个简单的辅助函数用来打印一些类型信息。函数的定义放在点C文件中:

//basics/myfirst.cpp

#include<iostream>

#include<typeinfo>

#include"myfirst.hpp"

//implementation/definitionoftemplate

template<typenameT>

voidprint_typeof(Tconst&x)

{

std::cout<<typeid(x).name()<<std::endl;

}

这个例子使用typeid操作符来打印一个字符串,这个字符串描述了传入的参数的类型信息。

最后,我们在另外一个点C文件中使用我们的模板,在这个文件中模板声明被#include:

//basics/myfirstmain.cpp

#include"myfirst.hpp"

//useofthetemplate

intmain()

{

doubleice=3.0;

print_typeof(ice);//callfunctiontemplatefortypedouble

}

大部分C++编译器(Compiler)很可能会接受这个程序,没有任何问题,但是链接器(Linker)大概会报告一个错误,指出缺少函数print_typeof()的定义。

这个错误的原因在于,模板函数print_typeof()的定义还没有被具现化(instantiate)。为了具现化一个模板,编译器必须知道
哪一个定义应该被具现化,以及使用什么样的模板参数来具现化。不幸的是,在前面的例子中,这两组信息存在于分开编译的不同文件中。因此,当我们的编译器看
到对print_typeof()的调用,但是没有看到此函数为double类型具现化的定义时,它只是假设这样的定义在别处提供,并且创建一个那个定义
的引用(链接器使用此引用解析)。另一方面,当编译器处理myfirst.cpp时,该文件并没有任何指示表明它必须为它所包含的特殊参数具现化模板定
义。

头文件中的模板

解决上面这个问题的通用解法是,采用与我们使用宏或者内联函数相同的方法:我们将模板的定义包含进声明模板的头文件中。对于我们的例子,我们可以通
过将#include
"myfirst.cpp"添加到myfirst.hpp文件尾部,或者在每一个使用我们的模板的点C文件中包含myfirst.cpp文件,来达到目
的。当然,还有第三种方法,就是删掉myfirst.cpp文件,并重写myfirst.hpp文件,使它包含所有的模板声明与定义:

//basics/myfirst2.hpp

#ifndefMYFIRST_HPP

#defineMYFIRST_HPP

#include<iostream>

#include<typeinfo>

//declarationoftemplate

template<typenameT>

voidprint_typeof(Tconst&);

//implementation/definitionoftemplate

template<typenameT>

voidprint_typeof(Tconst&x)

{

std::cout<<typeid(x).name()<<std::endl;

}

#endif//MYFIRST_HPP

这种组织模板代码的方式就称作包含模式。经过这样的调整,你会发现我们的程序已经能够正确编译、链接、执行了。

从这个方法中我们可以得到一些观察结果。最值得注意的一点是,这个方法在相当程度上增加了包含myfirst.hpp的开销。在这个例子中,这种开
销并不是由模板定义自身的尺寸引起的,而是由这样一个事实引起的,即我们必须包含我们的模板用到的头文件,在这个例子中
是<iostream>和<typeinfo>。你会发现这最终导致了成千上万行的代码,因为诸
如<iostream>这样的头文件也包含了和我们类似的模板定义。

这在实践中确实是一个问题,因为它增加了编译器在编译一个实际程序时所需的时间。我们因此会在以后的章节中验证其他一些可能的方法来解决这个问题。但无论如何,现实世界中的程序花一小时来编译链接已经是快的了(我们曾经遇到过花费数天时间来从源码编译的程序)。

抛开编译时间不谈,我们强烈建议如果可能尽量按照包含模式组织模板代码。

另一个观察结果是,非内联模板函数与内联函数和宏的最重要的不同在于:它并不会在调用端展开。相反,当模板函数被具现化时,会产生此函数的一个新的
拷贝。由于这是一个自动的过程,编译器也许会在不同的文件中产生两个相同的拷贝,从而引起链接器报告一个错误。理论上,我们并不关心这一点:这是编译器设
计者应当关心的事情。实际上,大多数时候一切都运转正常,我们根本就不用处理这种状况。然而,对于那些需要创建自己的库的大型项目,这个问题偶尔会显现出
来。

最后,需要指出的是,在我们的例子中,应用于普通模板函数的方法同样适用于模板类的成员函数和静态数据成员,以及模板成员函数。

转自http://blog.pfan.cn/liangdas/38886.html
-------------------------------------------------------------------------------------------

C++中的模板函数

<引言>

函数模板是C++中的高级概念,初级用户即使使用了一些标准库中的函数模板都未必意识到。

作为强类型语言的C++,很多时候往往制约了一些较强语言功能的发挥,比如在Perl/PHP等脚本语言中,类型的模糊为实现普遍的算法

(比如min/max)提供了较好支持,同样的算法可以施加在各种不同数据类型上,而讲这些差别交给解释器完成,无疑给用户提供了很大的

方便。在C中一般只能通过使用宏达到类似的效果,但显然这不是个类型安全和稳妥的方法。而C++就必须为各种类型编写函数(比如用

重载函数),更好的方法便是使用函数模板.


<定义>

template

<temp_par1,temp_par2,...>

temp_par是模板类型参数(template

typeparameter).它代表了一种类型,也可以是模板非类型参数(template

nontypeparameter),

它代表一个常量表达式.

template

typeparameter由class或typename后加一个标识符构成,在函数的模板参数表中这两个关键字的意义相同它们表示后面的

参数名代表一个潜在的内置或用户定义的类型,模板参数名由程序员选择譬如

template

<class

Glorp>

Glorpmin(Glorpa,Glorpb){

return

a<b?a:b;

}

template

nontypeparameter则在模板实例化期间是常量

如同非模板函数一样函数模板也可以被声明为inline或extern应该把指示符放在模板参数表后面而不是在关键字template前面,如:

template

<class

Glorp>

inline

Glorpmin(Glorpa,Glorpb){

return

a<b?a:b;

}



<关于typename和class>

为了分析模板定义,编译器必须能够区分出碰到的表达式是一个计算表达式还是声明类型的表达式.对于编译器来说它并不是件容易的事情。

例如如果编译器在模板定义中遇到表达式Parm::name,且Parm这个模板类型参数代表了一个类,那么name引用的一定是

Parm的一个类型成员吗

template

<class

Parm,class

U>

Parmminus(Parm*array,Uvalue)

{

Parm::name*p;//这是一个指针声明还是乘法乘法

}

显然,编译器不知道name是否为一个类型,因为它只有在模板被实例化之后才能找到Parm表示的类的定义.为了让编译器能够分析模板

定义,用户必须指示编译器哪些表达式是类型表达式.告诉编译器一个表达式是类型表达式的机制是在表达式前加上关键字typename.

例如:如果我们想让函数模板minus()的表达式Parm::name是个类型名,因而使整个表达式是一个指针声明我们应如下修改

template

<class

Parm,class

U>

Parmminus(Parm*array,Uvalue)

{

typename

Parm::name*p;//ok:指针声明

}

这其实就是C++的关键字typename之所以出现的最初原因,现在则主要用在模板参数表中以指示一个模板参数是一个类型.


<实例化>

函数模板在它被调用或取其地址时被实例化


<模板实参推演template

argumentsdeducation>


在模板实参推演期间决定模板实参的类型时,编译器不考虑函数模板实例的返回类型

模板实参推演的通用算法如下

1

依次检查每个函数实参,以确定在每个函数参数的类型中出现的模板参数

2

如果找到模板参数,则通过检查函数实参的类型推演出相应的模板实参

3

函数参数类型和函数实参类型不必完全匹配.下列类型转换可以被应用在函数实参上,以便将其转换成相应的函数参数的类型

A.左值转换(数组到指针,函数到指针)

B.限定修饰转换(const

,volatile

)

C.从派生类到基类类型的转换.

4

如果在多个函数参数中找到同一个模板参数,则从每个相应函数实参推演出的模板实参必须相同

从这里可以看出,模板实参的推演并不允许有序标准转换,因为这是在确定实例化哪个函数,而不是确定调用哪个函数


<显式模板实参(explicitlyspecify)>

比如:sum<char

,unsigned

int

,int

>(a,b,c)

此时,就没有必要推演模板实参了,函数参数的类型已经固定。当函数模板实参被显式指定时,把函数实参转换成相应函数参数

的类型可以应用任何隐式类型转换(当然包括有序标准转换)

显式模板实参应该只被用在完全需要它们来解决二义性,或在模板实参不能被推演出来的上下文中时


<模板编译模式>

包括InclusionModel和SeparationModel,

对于前者,每个模板被实例化的文件中包含模板定义,并且定义通常放在头文件中,就和inline函数的定义一样。缺点是,

没有把实现和声明分离开,并且可能导致同样的代码被多次编译,同时造成多次实例化(这可以通过显示实例化来解决).

对于后者,模板函数实现放在单独的实现文件中,头文件只是模板函数的声明。但模板函数实现的定义需要出现export关键字。

这样的好处是分开了接口和实现,缺点是需要仔细规划代码,尽量防止在多个文件中出现export同一个函数模板,这有可能造成

链接错误,另外一个遗憾是,目前很少有编译器支持SeparationModel(也许gcc或者VC7已经支持了)

<显式实例化声明>

为了是模板更加实用,标准C++提供这个机制帮助用户自行确定函数实例化的时机,用法如下:

template

<typename

Type>

Typesum(Typeop1,int

op2){/*

...

*/

}

//显式实例化声明

template

int

*sum<int

*>(int

*,int

);

则int*的函数被显式实例化.对于给定的函数模板实例,显式实例化声明在一个程序中只能出现一次,在显式实例化声明所在

的文件中函数模板的定义必须被给出.如果该定义不可见则该显式实例化声明是错误的.

显式实例化声明是与另外一个编译选项联合使用的.该选项压制了程序中模板的隐式实例化.选项的名称随着编译器不同而不同.

(感觉这个机制并不是很有用,不知道gcc的选项是什么)


<模板显式特化(explicit

specializationdefinition)>


虽然模板已经为我们做了很多有益的工作,但有的时候,我们不希望这个模板函数实例化我们所有的类型,通常是因为有些类型

如果例外处理反而对性能和功能有更大的好处。比如说,对一个比较相等函数来说,对于普通类型(int,bool),它们的操作基

本上是bitwise的比较,而对于C风格的字符串类型(char*),我们需要的则是memberwise的比较。因此C++提供模板显示特化

机制,给我们一个“重载”的机会。

例如:

//通用的模板定义

template

<class

T>

Tmax(Tt1,Tt2){

return

(t1>t2?t1:t2);

}

//constchar*显式特化:

//覆盖了来自通用模板定义的实例

typedef

const

char

*PCC;

template

<>PCCmax<PCC>(PCCs1,PCCs2){

return

(strcmp(s1,s2)>0

?s1:s2);

}

注意:在源文件中使用函数模板显式特化之前必须先进行声明

通常,模板显式特化的声明被包含在每个用需要被特化的类型实参调用函数模板的文件中.显式特化的声明应该被放在头文件中,

并在所有使用函数模板的程序中包含这个文件


<重载和函数模板>

函数模板同样可以被重载,只是名字解析由于模板实参推演过程的加入而变得稍许复杂了。主要的步骤如下:

1.

编译器看到一个函数调用点时,先确定所有同名的调用点的函数。除了普通的重载函数,候选函数集中还要再加上那些

实参推演成功的模板函数,此时如果存在模板特化,则实际上该模板特化成为一个候选函数。

2.

此时确定可选函数集(根据3

种标准转换),这一步和普通的重载解析一致。

3.

对各种转换进行分级打分,最后剩下的函数中,如果同时有普通函数和模板函数入选,则只取普通函数,如果普通函数不止

一个,则编译报错。显然,如果最后剩下的函数中没有模板函数,那么结果和一般的重载函数解析一致,即如果不止一个函数满

足要求,编译报错。

这里有一个问题可能会引起困惑。为什么同时有普通函数和模板函数入选时要优先考虑普通函数。这是为了应付这种情况,当

我们不得不对某种类型采取显式模板实参调用模板函数时,为了不修改整个文件中的多处函数调用,就直接用一个普通函数来声明,

而在普通函数里一次性完成显示模板实参调用。如以下所示:

//函数模板定义

template

<class

Type>

Typemin(Typet1,Typet2){...}

//普通函数

int

min(int

a1,int

a2){

min<int

>(a1,a2);

}

int

main(){

//调用普通函数

min(ai[0

],ss);

}

设想,如果在源程序很多地方都需要调用min<int

>(int

,int

),这不失为一个比较好的利用语言规则的措施,因为即使不利用这样的

规则,对程序员来说使用流行的编辑器软件完全替换所有函数调用点也绝对是不难的事情。


<模板定义中的名字解析>

在模板定义中,有些名字符号是需要到模板实例化时才能解析的,它们一般是模板的实参类型。而有些则是在模板定义时就可以

解析的。前者通常称为依赖于模板参数的名字(dependonatemplate

parameter),而后者显然是不依赖于模板参数的。

对于不依赖于模板参数的函数调用,必须在定义模板前事先声明。

而对于依赖模板参数的函数调用,则必须在模板实例化前声明。

因此,显而易见的推论是,不依赖模板参数的名字都由模板库的提供者声明,而用户只在需要实例化时给出名字的声明即可。

<名字空间和函数模板>

实际上很简单,把函数模板定义在一个名字空间里,就和把一个普通函数定义在名字空间里一样,使用前,或者using一下需要的函数,

或者把整个名字空间一起using进来。

这两者实际上没有本质的关联。


转自http://blog.csdn.net/lase/archive/2004/08/12/72795.aspx
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: