模板
函数模板
函数模板是一种特殊的函数,它可以操作
泛型类型。这允许我们创建一个函数模板,其功能可以适应多种类型或类,而无需为每种类型重复整个代码。
在 C++ 中,这可以使用
模板参数来实现。模板参数是一种特殊的参数,可用于传递类型作为参数:就像常规函数参数可用于将值传递给函数一样,模板参数允许也将类型传递给函数。这些函数模板可以使用这些参数,就像它们是任何其他常规类型一样。
声明带有类型参数的函数模板的格式是
template <class identifier> function_declaration;
template <typename identifier> function_declaration;
两种原型之间的唯一区别在于是否使用关键字
类class
或关键字typename。 它的使用是无区别的,因为这两个表达式具有完全相同的含义并且行为完全相同。
例如,要创建一个模板函数来返回两个对象中较大的一个,我们可以使用
1 2 3 4
|
template <class myType>
myType GetMax (myType a, myType b) {
return (a>b?a:b);
}
|
在这里,我们创建了一个具有
myType作为其模板参数的模板函数。 此模板参数表示尚未指定的类型,但可以在模板函数中使用,就像它是常规类型一样。 正如您所看到的,函数模板
GetMax返回此尚未定义的类型的两个参数中较大的一个。
要使用此函数模板,我们使用以下格式进行函数调用
function_name <type> (parameters);
例如,要调用
GetMax来比较类型为
int的两个整数值,我们可以写
1 2
|
int x,y;
GetMax <int> (x,y);
|
当编译器遇到对模板函数的此调用时,它使用该模板自动生成一个函数,将每次出现的
myTypemyType
int替换为作为实际模板参数传递的类型 (在本例中为int),然后调用它。此过程由编译器自动执行,程序员不可见。
这是完整的例子
|
// function template
#include <iostream>
using namespace std;
template <class T>
T GetMax (T a, T b) {
T result;
result = (a>b)? a : b;
return (result);
}
int main () {
int i=5, j=6, k;
long l=10, m=5, n;
k=GetMax<int>(i,j);
n=GetMax<long>(l,m);
cout << k << endl;
cout << n << endl;
return 0;
}
|
6
10 |
在本例中,我们使用了
T作为模板参数名称,而不是
myTypemyType,因为它更短,而且实际上是一个非常常见的模板参数名称。 但是您可以使用任何您喜欢的标识符。
在上面的例子中,我们使用了函数模板
GetMax()两次。 第一次使用
intint
类型的参数,第二次使用类型的参数。 编译器已经实例化,然后每次都调用了函数的适当版本。
正如您所看到的,类型
TT
GetMax()在模板函数中甚至用于声明该类型的新对象
因此,
result将是与参数
a和
和 b在函数模板使用特定类型实例化时相同的类型的对象。
在这个特定情况下,泛型类型
TT
GetMax用作GetMax()的参数,编译器可以自动找出必须实例化的数据类型,而无需在尖括号内明确指定它(就像我们之前指定<int>和<long>一样)。
<int>和
<long>)。 所以我们可以写成
1 2
|
int i,j;
GetMax (i,j);
|
因为
i和
和j
int的类型都是int,编译器可以自动找出模板参数只能是int。 这种隐式方法产生完全相同的结果。
int这种隐式方法产生完全相同的结果
|
// 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;
}
|
6
10 |
请注意,在这种情况下,我们调用了函数模板
GetMax()GetMax
<>而没有显式指定尖括号之间的类型。 编译器自动确定每次调用需要什么类型。
因为我们的模板函数仅包含一个模板参数 (
class T) 并且函数模板本身接受两个参数,这两个参数都是这种类型,因此我们不能使用两种不同类型的对象作为参数调用我们的函数模板。
T类型,因此我们不能使用两种不同类型的对象作为参数调用我们的函数模板。
1 2 3
|
int i;
long l;
k = GetMax (i,l);
|
这是不正确的,因为我们的
GetMax函数模板需要两个相同类型的参数,而在这次调用中我们使用了两种不同类型的对象。
我们还可以定义接受多个类型参数的函数模板,只需在尖括号之间指定更多模板参数即可。 例如
1 2 3 4
|
template <class T, class U>
T GetMin (T a, U b) {
return (a<b?a:b);
}
|
在这种情况下,我们的函数模板
GetMin()接受两种不同类型的参数,并返回与传递的第一个参数 (T) 相同类型的对象。 例如,在该声明之后,我们可以调用
TGetMin(i,l)
GetMin()使用
1 2 3
|
int i,j;
long l;
i = GetMin<int,long> (j,l);
|
GetMin(i,l)
甚至可以简单地
和和
GetMin(i,l)尽管i和l具有不同的类型,因为编译器无论如何都可以确定适当的实例化。
类模板
我们还可以编写类模板,以便类可以具有使用模板参数作为类型的成员。 例如
1 2 3 4 5 6 7 8 9
|
template <class T>
class mypair {
T values [2];
public:
mypair (T first, T second)
{
values[0]=first; values[1]=second;
}
};
|
我们刚刚定义的类用于存储任何有效类型的两个元素。 例如,如果我们想声明此类的对象以存储两个int类型的值 115 和 36,我们将编写
intPair
myints (115,36);
1
|
mypair<int> myobject (115, 36);
|
这个类同样可以用来创建一个存储任何其他类型的对象
1
|
mypair<double> myfloats (3.0, 2.18);
|
前一个类模板中唯一的成员函数已在类声明本身内联定义。 如果我们在类模板声明之外定义函数成员,我们必须始终在定义之前加上template <...>前缀
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
|
// 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;
}
|
100 |
注意成员函数 getmax 的语法
1 2
|
template <class T>
T mypair<T>::getmax ()
|
被这么多的T搞糊涂了吗?T这个声明中有三个 T:第一个 T 是模板参数。 第二个T指的是函数返回的类型。 第三个T(尖括号中的那个)也是一个要求:它指定此函数的模板参数也是类模板参数。T这个声明中有三个T's在声明中:第一个是模板参数。第二个T指的是函数返回的类型。第三个
模板特化
如果我们想为模板定义一个不同的实现,当传递一个特定类型作为模板参数时,我们可以声明该模板的特化。
例如,假设我们有一个非常简单的类叫做mycontainer它可以存储任何类型的一个元素,并且它只有一个成员函数叫做increase,它增加它的值。但是我们发现当它存储一个类型为char的元素时,使用一个具有函数成员uppercase的完全不同的实现会更方便,因此我们决定为该类型声明一个类模板特化。
|
// 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;
}
|
8
J |
这是类模板特化中使用的语法
1
|
template <> class mycontainer <char> { ... };
|
首先,请注意我们在类模板名称之前加上一个空的template<>参数列表。这是为了明确地将其声明为模板特化。
但比此前缀更重要的是,是类模板名称后面的<char><char>特化参数。此特化参数本身标识了我们将要声明模板类特化的类型 (char)。请注意通用类模板和特化之间的差异charchar。
1 2
|
template <class T> class mycontainer { ... };
template <> class mycontainer <char> { ... };
|
第一行是通用模板,第二行是特化。
当我们为模板类声明特化时,我们还必须定义它的所有成员,即使那些与通用模板类完全相同的成员,因为从通用模板到特化没有成员的“继承”。
模板的非类型参数
除了以类class或关键字或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;
}
|
100
3.1416 |
也可以为类模板参数设置默认值或类型。例如,如果之前的类模板定义是
1
|
template <class T=char, int N=10> class mysequence {..};
|
我们可以通过声明使用默认模板参数创建对象
这相当于
1
|
mysequence<char,10> myseq;
|
模板和多文件项目
从编译器的角度来看,模板不是普通的函数或类。 它们是按需编译的,这意味着直到需要使用特定模板参数进行实例化时,才编译模板函数的代码。 在需要实例化的时候,编译器会从模板中为这些参数专门生成一个函数。
当项目增长时,通常将程序的代码拆分为不同的源代码文件。 在这些情况下,接口和实现通常是分开的。 以函数库为例,接口通常包含所有可以调用的函数的原型声明。 这些通常在带有 .h 扩展名的“头文件”中声明,而实现(这些函数的定义)在带有 c++ 代码的独立文件中。
因为模板是在需要时编译的,这迫使多文件项目受到限制:模板类或函数的实现(定义)必须与其声明在同一个文件中。 这意味着我们不能将接口分成单独的头文件,并且我们必须在使用模板的任何文件中包含接口和实现。
由于在需要时实例化模板之前不会生成任何代码,因此编译器已准备好允许在项目中多次包含具有声明和定义的相同模板文件,而不会生成链接错误。