大约是受宏(预处理)的压迫太深,所以有了模板以后的第一件事情便是定义模板函数来取代那些MAX和MIN。作者也乐得如此,下面我就马上来看一个最简单的模板函数max的实现:
如果费力的逐个解释一下的话,那大概是这样:template表明了这是一个模板函数,<>指定了模板参数区域,typename表明了后面的参数是一个类型名,而T则表明了一种广泛意义上的类型,它可以用来指定所有的类型(int, float, std::string)。所有的这些构成了一个最基本的模板函数,需要说明的一点是由于历史的原因,这里的typename可以用class来取代。(我们确实看到很多都是用class来描述的)。当然也有人问了,那struct是不是也可以,答案当然是不可以!
了解了这个函数的定义,第二步要做的就是探索如何调用它。这里有几个例子:
第一个和第二个调用没有任何问题,就像普通的函数调用,当然他们的调用是可以成功的。但是到第三个调用的时候,问题出现了,编译器报了一个错误'const T &max(const T &,const T &)' : template parameter 'T' is ambiguous。可以看到这里的参数类型出了问题,两个参数的类型不一致。一个是int一个double,所以编译器不知道怎么指定T,注意这里编译器需要的是一个确切的类型,所以他是不会主动给你做任何类型转化的。那怕从int32到int16都不会。所以在这里你需要显示地告诉编译器你要怎么做,这就是第四个调用max<double>(1, 2.5);或者你也可以强制转化一下参数1,让他转化成double然后调用,比如说max(static_cast<double>1, 2.5)。
更进一步,我们来看看编译器是如何帮助我们实现这些调用的。简单的说,编译器采用的办法就是存在什么类型的调用,我就给你产生一堆什么类型对应的max代码。比如说max(1,2);调用来的时候,我就给你生成一段这样的代码:
就是用int来替代了原来的T产生了具体类型的代码,这样原来程序员需要为不同类型版本写函数的工作就交给了编译器来做。编译器为不同类型生成对应函数代码的过程被称之为实例化(Instantiation,很巧和OO里面的实例化是同一个词。当然意义是决然不同的)。接下来我们可以看看一个复杂一点的例子:
这是一个完整的运行在VS2005下的例子。这里的不同就是模板函数有了不同的重载的版本,这里在实例化之前,就需要为不同的类型找到一个最合适的函数调用,比如说调用max(a,b);的时候,a,b都是int,编译器发现第一个max函数,也就是以value作为参数的版本最为匹配,所以会就此实例化出一个第一个函数的int版本,注意这里这种匹配的流程被称之为演绎(Deduction)。而演绎的基本的原则就是首先匹配并存的非模板函数,如果不成功就寻找最接近的模板函数。比如max(s1,s2);的时候,匹配到的就是const char*的那个版本的max函数,而max(a1, a2);的时候就是第二个max函数,也就是以指针作为类型参数的版本。
到目前为止还是一派鸟语花香,聪明的编译器做到我们希望的一切,max的表现和预想的出奇的一致。当然不得不提的一点就是平静之下的惊涛暗涌。且看下面的例子:
可以看到如果这些模板参数类型不匹配的话,带来的问题有多大,可以看到max(7,42,68);的时候没有问题,3个参数的max函数匹配到第一个max函数,这里没有问题,大家都是传引用的语义。但是到max(s1,s2,s3);的时候,那个max(max(a,b),c);匹配到了const char*的max版本,这样,max的返回值就从引用改成了栈上值。尽管这里的comment写了ERROR,但实际上VS2005中只会报一个returning address of local variable or temporary的warning!那些以error为判断条件的筒子们应该注意了,其实这样的warning也会是致命的。所以聪明的编译器带给我们的并不是总是放心的。