函数模板
函数模板是可以对泛型类型进行操作的特殊函数。这允许我们创建一个函数模板,它的功能可以适应多个类型或类,而不需要为每个类型重复整个代码。
在c++中,这可以通过使用模板参数来实现。模板参数是一种特殊类型的参数,可用于将类型作为参数传递:就像常规函数参数可用于将值传递给函数一样,模板参数也允许将类型传递给函数。这些函数模板可以像使用任何其他常规类型一样使用这些参数。
使用类型参数声明函数模板的格式为:
template function_declaration;
template function_declaration;
这两个原型之间唯一的区别是使用关键字类或关键字typename。它的使用是模糊的,因为这两个表达式有完全相同的含义和行为完全相同的方式。
例如,创建一个模板函数,返回两个对象中较大的一个:
template <class myType>
myType GetMax (myType a, myType b) {
return (a>b?a:b);
}
这里我们创建了一个模板函数,其中myType作为模板参数。此模板参数表示尚未指定的类型,但可以在模板函数中使用该类型,就像它是常规类型一样。如您所见,函数模板GetMax返回这个仍然未定义类型的两个参数中较大的一个。
要使用这个函数模板,我们对函数调用使用以下格式:
例如,要调用GetMax来比较int类型的两个整数值,我们可以这样写:
int x,y;
GetMax (x,y);
当编译器遇到对模板函数的这个调用时,它使用模板自动生成一个函数,用作为实际模板参数(在本例中是int)传递的类型替换myType的每个外观,然后调用它。这个过程由编译器自动执行,程序员看不到它。
下面是整个例子:
// function template II
#include <iostream>
using namespace std;
template <class T>
T GetMax (T a, T b) {
return (a>b?a:b);
}
int main () {
int i=5, j=6, k;
long l=10, m=5, n;
k=GetMax(i,j);
n=GetMax(l,m);
cout << k << endl;
cout << n << endl;
return 0;
注意,在本例中,我们调用函数模板GetMax(),而没有显式地指定角括号<>之间的类型。编译器自动确定每次调用所需的类型。
因为我们的模板函数只包含一个模板参数(类T),而函数模板本身接受两个参数,都是这个T类型,所以我们不能用两个不同类型的对象作为参数来调用我们的函数模板:
int i;
long l;
k = GetMax (i,l);
这是不正确的,因为我们的GetMax函数模板需要两个相同类型的参数,在对它的调用中,我们使用了两个不同类型的对象。
我们还可以定义接受多个类型参数的函数模板,只需在尖括号之间指定更多的模板参数。例如:
template <class T, class U>
T GetMin (T a, U b) {
return (a<b?a:b);
}
在本例中,函数模板GetMin()接受两个不同类型的参数,并返回与传递的第一个参数(T)相同类型的对象。例如,在声明之后,我们可以调用GetMin():
int i,j;
long l;
i = GetMin<int,long> (j,l)
或者简单的
i = GetMin (j,l);
尽管j和l有不同的类型,因为编译器无论如何都可以确定适当的实例化。
类模板
我们还可以编写类模板,这样类就可以拥有使用模板参数作为类型的成员。例如:
template <class T>
class mypair {
T values [2];
public:
mypair (T first, T second)
{
values[0]=first; values[1]=second;
}
};
我们刚刚定义的类用于存储任何有效类型的两个元素。例如,如果我们想声明这个类的一个对象来存储两个int类型的整数值,值分别为115和36,我们可以这样写:
mypair<int> myobject (115, 36);
mypair<double> myfloats (3.0, 2.18);
前一个类模板中唯一的成员函数已在类声明本身内内联定义。如果我们在类模板的声明之外定义了一个函数成员,我们必须始终在该定义之前加上模板<…>前缀:
// class templates
#include <iostream>
using namespace std;
template <class T>
class mypair {
T a, b;
public:
mypair (T first, T second)
{a=first; b=second;}
T getmax ();
};
template <class T>
T mypair<T>::getmax ()
{
T retval;
retval = a>b? a : b;
return retval;
}
int main () {
mypair <int> myobject (100, 75);
cout << myobject.getmax();
return 0;
}
定义成员函数:
template <class T>
T mypair<T>::getmax ()
被这么多的T搞糊涂了?这个声明中有三个T:第一个是模板参数。第二个T表示函数返回的类型。第三个T(尖括号之间的那个)也是一个要求:它指定这个函数的模板参数也是类模板参数。
模板特殊化
如果希望在传递特定类型作为模板参数时为模板定义不同的实现,可以声明该模板的专门化。
例如,假设我们有一个非常简单的类mycontainer,它可以存储任何类型的一个元素,并且它只有一个成员函数increase,这个函数增加了它的值。但是我们发现,当它存储char类型的元素时,使用函数成员大写的完全不同的实现会更方便,所以我们决定为该类型声明一个类模板专门化:
// template specialization
#include <iostream>
using namespace std;
// class template:
template <class T>
class mycontainer {
T element;
public:
mycontainer (T arg) {element=arg;}
T increase () {return ++element;}
};
// 模板特殊化
// class template specialization:
template <>
class mycontainer <char> {
char element;
public:
mycontainer (char arg) {element=arg;}
char uppercase ()
{
if ((element>='a')&&(element<='z'))
element+='A'-'a';
return element;
}
};
int main () {
mycontainer<int> myint (7);
mycontainer<char> mychar ('j');
cout << myint.increase() << endl;
cout << mychar.uppercase() << endl;
return 0;
}
这是在类模板专门化中使用的语法:
template <> class mycontainer <char> { ... };
首先,请注意,我们在类模板名称前面加上一个空模板<>参数列表。这是显式地将其声明为模板专门化。
但是比这个前缀更重要的是类模板名称后面的专门化参数。这个专门化参数本身标识了我们将为其声明模板类专门化(char)的类型。注意泛型类模板和专门化之间的区别:
template <class T> class mycontainer { ... };
template <> class mycontainer <char> { ... };
第一行是泛型模板,第二行是专门化。
当我们为模板类声明专门化时,我们还必须定义它的所有成员,甚至那些与泛型模板类完全相等的成员,因为从泛型模板到专门化没有成员的“继承”。
模板的非类型参数
除了前面有表示类型的类或typename关键字的模板参数之外,模板还可以有规则的类型化参数,类似于在函数中找到的参数。例如,看看这个类模板,它用来包含元素序列:
// sequence template
#include <iostream>
using namespace std;
template <class T, int N>
class mysequence {
T memblock [N];
public:
void setmember (int x, T value);
T getmember (int x);
};
template <class T, int N>
void mysequence<T,N>::setmember (int x, T value) {
memblock[x]=value;
}
template <class T, int N>
T mysequence<T,N>::getmember (int x) {
return memblock[x];
}
int main () {
mysequence <int,5> myints;
mysequence <double,5> myfloats;
myints.setmember (0,100);
myfloats.setmember (3,3.1416);
cout << myints.getmember(0) << '\n';
cout << myfloats.getmember(3) << '\n';
return 0;
}
还可以为类模板参数设置默认值或类型。例如,如果之前的类模板定义是:
template <class T=char, int N=10> class mysequence {..};
我们可以通过声明:
mysequence < > myseq;
这相当于:
mysequence<char,10> myseq;
模板和多文件项目
从编译器的角度来看,模板不是普通的函数或类。它们是按需编译的,这意味着在需要使用特定模板参数实例化之前,不会编译模板函数的代码。此时,当需要实例化时,编译器将从模板中为这些参数生成一个专门的函数。
当项目增长时,通常将程序的代码分割到不同的源代码文件中。在这些情况下,接口和实现通常是分开的。以函数库为例,接口通常由所有可调用函数的原型声明组成。这些函数通常在一个扩展名为.h的“头文件”中声明,实现(这些函数的定义)在一个带有c++代码的独立文件中。
因为模板是在需要时编译的,这就对多文件项目施加了限制:模板类或函数的实现(定义)必须与其声明位于同一个文件中。这意味着我们不能在单独的头文件中分离接口,并且必须在使用模板的任何文件中同时包含接口和实现。
由于在需要时实例化模板之前不会生成代码,所以编译器准备允许在项目中包含多个具有声明和定义的相同模板文件,而不会生成链接错误。