函数模板
函数模板是可以操作通用类型的特殊函数。这使得我们可以创建一个功能可以适应多个类型或类的函数模板,而无需为每个类型重复整个代码。
在C++中,这可以通过模板参数来实现。模板参数是一种特殊的参数,可以用来传递一个类型作为参数:就像普通的函数参数可以用来传递数值给一个函数,模板参数也可以传递类型给一个函数。这些函数模板可以使用这些参数,就像它们是任何其他常规类型一样。
声明带类型参数的函数模板的格式是。
template <class identifier> function_declaration;
template<typename identifier> function_declaration。
这两种原型的唯一区别是使用了关键字class或关键字tyename。这两个使用上没有明确的说法,因为这两个表达式的含义完全相同,行为方式也完全相同。
例如,要创建一个模板函数,返回两个对象中较大的一个,我们可以使用:
template <class myType>
myType GetMax(myType a, myType b) {
return (a>b?a:b);
}
这里我们创建了一个模板函数,myType是其模板参数。这个模板参数代表一个尚未指定的类型,但可以在模板函数中使用,就像它是一个普通类型一样。正如你所看到的,函数模板GetMax返回这个还未定义的类型的两个参数中的较大者。
为了使用这个函数模板,我们使用以下格式的函数调用。
function_name <type> (parameters);
例如,要调用GetMax来比较两个int类型的整数值,我们可以这样写:
int x, y;
GetMax<int>(x, y);
当编译器遇到这种对模板函数的调用时,它会使用模板自动生成一个函数,用作为实际模板参数传递的类型(本例中为int)替换myType的每个外观,然后调用它。这个过程是由编译器自动执行的,程序员是看不到的。
下面是整个例子:
// 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;
}
在这种情况下,我们使用T作为模板参数名称,而不是myType,因为它比较短,事实上是一个非常常见的模板参数名称。但你可以使用任何你喜欢的标识符。
在上面的例子中,我们使用了两次函数模板GetMax()。第一次使用int类型的参数,第二次使用long类型的参数。编译器已经实例化了,然后每次都调用了适当版本的函数。
正如你所看到的,类型T在GetMax()模板函数中甚至被用来声明该类型的新对象。
T result;
因此,当函数模板被实例化为特定类型时,结果将是一个与参数a和b相同类型的对象。
在这个特定的案例中,通用类型T被用作GetMax的参数,编译器可以自动找出需要实例化的数据类型,而不必在角括号中明确指定(就像我们之前指定<int>和<long>那样)。所以我们可以这样写:
int i, j;
GetMax(i, j);
因为i和j都是int类型,而且编译器可以自动发现模板参数只能是int。这种隐式方法产生的结果完全相同。
请注意,在这种情况下,我们调用了我们的函数模板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(尖括号之间的那个)也是需要的:它规定了这个函数的模板参数也是类的模板参数。
另外,和函数模板不同的是,使用类的模板,在创建实例时,是没法隐式推断模板类型的。
比如下述方法就是错的:
mypair myobject (100, 75);
模板的特例化 / Template specialization
当一个特定的类型作为模板参数被传递时,如果我们想为一个模板定义一个不同的实现,我们可以声明一个该模板的特例。
例如,假设我们有一个非常简单的类,叫做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;
/* compile error */
//cout << mychar.increase() << endl;
return 0;
}
当我们为一个模板类声明特例时,我们也必须定义它的所有成员,甚至那些与通用模板类完全相同的成员,因为从通用模板到特例的成员没有 "继承"。
使用类模板特例的语法是:
template<> class mycontainer<char> { ... };
首先,注意到我们在类的模板名称前加了一个空的模板<>参数列表。这是为了明确地声明它是一个模板的特例。
但比这个前缀更重要的是类模板名称后面的<char>特殊化参数。这个特殊化参数本身标识了我们要声明的模板类特殊化的类型(char)。注意通用类模板和特例之间的区别。
template <class T> class mycontainer { ... };
template <> class mycontainer <char> { ... };
第一行是通用模板,第二行是特例。
在模板中使用非类型参数
除了在模板参数前加上代表类型的class或tyename关键字外,模板还可以有常规的类型参数,类似于函数中的参数。下面的例子,将元素个数作为模板参数:
// 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) << endl;
return 0;
}
也可以为类模板参数设置默认值或类型。例如,如果之前的类模板定义是:
template <class T=char, int N=10> class mysequence {..};
我们可以通过声明,使用默认的模板参数创建对象:
mysequence<> myseq;
这将等同于:
mysequence<char,10> myseq;
模板在多文件项目中的使用
从编译器的角度来看,模板不是普通的函数或类。它们是按需编译的,这意味着模板函数的代码不会被编译,直到需要用特定模板参数进行实例化。此时,当需要实例化时,编译器会从模板中为这些参数专门生成一个函数。
当项目增长时,通常会将一个程序的代码分成不同的源代码文件。在这种情况下,接口和实现一般都是分开的。以一个函数库为例,接口一般由所有可以调用的函数的原型声明组成。这些通常在一个以.h为扩展名的 "头文件 "中声明,而实现(这些函数的定义)则在一个独立的带有c++代码的文件中。
因为模板是在需要时才编译的,这就迫使多文件项目受到限制:模板类或函数的实现(定义)必须与它的声明在同一个文件中。这意味着我们不能在一个单独的头文件中分离接口,而且我们必须在任何使用模板的文件中包含接口和实现。
由于在需要时模板被实例化之前不会产生代码,编译器允许在一个项目中多次包含具有声明和定义的同一模板文件而不产生链接错误。
参考:
Templates - C++ Tutorialshttps://www.cplusplus.com/doc/oldtutorial/templates/