函数模板
函数模板是通用的函数描述,他们使用泛型来定义函数,其中的泛型可用具体的类型(如int或double)替换。通过将类型作为参数传递给模板,可使编译器生成该类型得函数。
由于模板允许以泛型(而不是具体类型)的方式编写程序,因此有时也被称为通用编程。
由于类型是用参数表示的,因此模板特性优势也被称为参数化类型(parameterized types)。
函数模板允许以任意类型的方式来定义函数,例如,可以这样建立一个交换模板:
template <typename AnyType> //建立一个模板,并将类型命名为AnyType
//必须使用尖括号,类型名可以任意选择(这里为AnyType),很多人用T
//关键字template和typename是必需的,除非可以使用class代替typename
void Swap(AnyType &a, AnyType &b)
{
AnyType temp;
temp = a;
a = b;
b = temp;
}
在C++98添加关键字typename之前,C++使用关键字class来创建模板,即:
template <class AnyType>
void Swap(AnyType &a, AnyType &b)
{
AnyType temp;
temp = a;
a = b;
b = temp;
}
程序8.11
#include <iostream>
//function template prototype
template <typename T>
void Swap(T &a, T &b);
int main()
{
using namespace std;
int i = 10;
int j = 20;
cout << "i, j = " << i << ", " << j << ".\n";
cout << "Using complier-generated int swapper:\n";
Swap(i, j);
cout << "Now i, j = " << i << ", " << j << "\n";
double x = 24.5;
double y = 81.7;
cout << "x, y = " << x << ", " << y << ".\n";
cout << "Using complier-generated double swapper:\n";
Swap(x, y);
cout << "Now x, y = " << x << ", " << y << "\n";
system("pause");
return 0;
}
template <typename T>
void Swap(T &a, T &b)
{
T temp;
temp = a;
a = b;
b = temp;
}
重载的模板
需要多个对不同类型使用同一种算法的函数式,可以使用模板;然而,并非所有的类型都使用相同的算法,为满足这种需求,可以像重载常规函数那样定义重载模板。
和常规重载一样,被重载的模板的函数特征标必需不同。
程序8.12
#include <iostream>
template <typename T>
void Swap(T &a, T &b);
template <typename T>
void Swap(T *a, T *b, int n);
void Show(int a[]);
const int Lim = 8;
int main()
{
using namespace std;
int i = 10;
int j = 20;
cout << "i, j = " << i << ", " << j << ".\n";
cout << "Using complier-generated int swapper:\n";
Swap(i, j);
cout << "Now i, j = " << i << ", " << j << "\n";
int d1[Lim] = { 0,7,0,4,1,7,7,6 };
int d2[Lim] = { 0,7,2,0,1,9,6,9 };
cout << "Orginal arrays:\n";
Show(d1);
Show(d2);
Swap(d1, d2, Lim);
cout << "Swapped arrays:\n";
Show(d1);
Show(d2);
system("pause");
return 0;
}
template <typename T>
void Swap(T &a, T &b)
{
T temp;
temp = a;
a = b;
b = temp;
}
template <typename T>
void Swap(T *a, T *b, int n)
{
T temp;
for (int i = 0; i < n; i++)
{
temp = a[i];
a[i] = b[i];
b[i] = temp;
}
}
void Show(int a[])
{
using namespace std;
cout << a[0] << a[1] << "/";
cout << a[2] << a[3] << "/";
for (int i = 4; i < Lim; i++)
cout << a[i];
cout << endl;
}
模板的局限性
假设有如下模板函数:
template <class T> //or template <typename T>
void f(T a, T b)
{...}
通常,代码假定可执行哪些操作。例如下面的代码假定定义了赋值,但如果T为数组,这种假设将不成立:
a = b;
同样,下面的语句假定定义了<,但如果T为结构,该假设便不成立:
if (a>b)
总之,编写的模板函数很可能无法处理某些类型。另一方面,有时候通用化是有意义的,但C++语法不允许这样做。
显式具体化
假设定义了如下结构:
struct job
{
char name[40];
double salary;
int floor;
};
C++允许将一个结构赋给另一个结构,因此即使T是一个job结构,Swap()函数也同样适用。假设只想交换salary和floor成员,而不交换name成员,则需要使用不同的代码,但Swap()的参数将保持不变(两个job结构的引用),因此无法使用模板重载来提供其他的代码。
可以提供一个具体化的函数定义——称为显式具体化(explicit specialization),其中包含所需的代码。当编译器找到与函数调用匹配的具体化定义时,将使用该定义,而不再寻找模板。
- 对于给定的函数名,可以有非模板函数、模板函数、和显式具体化模板函数以及它们的重载版本;
- 显示具体化的原型和定义应以template<>打头,并通过名称来指出类型;
- 具体化优先于常规模板,而非模板函数优先于具体化和常规模板。
下面是用于交换job结构的非模板函数、模板函数和具体化的原型:
//非模板函数
void Swap(job &, job &);
//模板函数
template <typename T>
void Swap(T &,T &);
//具体化
template <> void Swap<job>(job &,job &);
Swap<job>
中的<job>
是可选的,因为函数的参数类型表明,这是job的一个具体化:
template <> void Swap(job &, job &);
程序8.13
#include <iostream>
template <typename T>
void Swap(T &a, T &b);
struct job
{
char name[40];
double salary;
int floor;
};
template <> void Swap<job>(job &j1, job & j2);
void Show(job &);
int main()
{
using namespace std;
cout.precision(2);
cout.setf(ios::fixed, ios::floatfield);
int i = 10, j = 20;
cout << "i, j = " << i << ", " << j << ".\n";
cout << "Using complier generated in swapper:\n";
Swap(i, j);
cout << "Now i, j = " << i << ", " << j << ".\n";
job sue = { "Susan Yaffee", 73000.60, 7 };
job sidney = { "Sidney Taffee", 78060.72, 9 };
cout << "Before job swapping:\n";
Show(sue);
Show(sidney);
Swap(sue, sidney);
cout << "After job swapping:\n";
Show(sue);
Show(sidney);
system("pause");
return 0;
}
template <typename T>
void Swap(T &a, T &b)
{
T temp;
temp = a;
a = b;
b = temp;
}
template <> void Swap<job>(job &j1, job &j2)
{
double t1;
int t2;
t1 = j1.salary;
j1.salary = j2.salary;
j2.salary = t1;
t2 = j1.floor;
j1.floor = j2.floor;
j2.floor = t2;
}
void Show(job &j)
{
using namespace std;
cout << j.name << ": $" << j.salary
<< " on floor " << j.floor << endl;
}
实例化和具体化
隐式实例化:在调用函数时,编译器生成模板的一个实力。
显式实例化:直接命令编译器创建特定的实例,如Swap<int>()
。其语法是。声明所需的种类,用<>指示类型,并在声明前加上关键字template:
template void Swap<int>(int,int);
与具体化的区别在于,显式具体化使用下面两个等价的声明之一:
template <> void Swap<int>(int &,int &);
template <> void Swap(int &,int &);
试图在同一个文件中使用同一种类型的显示实例化和显式具体化将出错。
还可通过在程序中使用函数来创建显式实例化:
template <class T>
T Add(T a, T b)
{
return a + b;
}
...
int m = 6;
doble x = 10.2;
cout << Add<double>(x,m)<<endl;
这里的模板与函数调用Add(x,m)不匹配,因为该模板要求两个函数参数的类型相同,但通过使用Add<double>(x,m)
可强制为double类型实例化。
隐式实例化、显示实例化与显式具体化统称为具体化。
...
template <class T>
void Swap (T &, T &);
template <> void Sawp<job>(job &, job &); //显式具体化
int main()
{
template void Swap<char>(char &, char &); //显式实例化
short a, b;
...
Swap(a,b); //隐式实例化for short
job n, m;
...
Swap(n,m); //使用显式具体化for job
char g, h;
...
Swap(g,h); //使用显式实例化for char
...
}
编译器选择使用哪个函数版本
对于函数重载、函数模板和函数模板重载,C++由一个良好的策略来决定函数调用使用哪一个函数定义,尤其是有多个参数时,这个过程称为重载解析。
- 创建候选函数列表,其中包含与被调用函数名称相同的函数和模板函数;
- 使用候选函数列表创建可行函数列表。
- 确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错。
最佳可行函数顺序:
- 完全匹配,但常规函数优先于模板;
- 提升转换,例如char和short自动转换为int,float自动转换为double;
- 标准转换,例如int转换为char,long转换为double;
- 用户定义的转换,如类声明中定义的转换。
程序8.14
#include <iostream>
template <typename T>
void ShowArray(T arr[], int n);
template <typename T>
void ShowArray(T * arr[], int n);
struct debts
{
char name[50];
double amount;
};
int main()
{
using namespace std;
int things[6] = { 13,31,103,301,310,130 };
struct debts mr_E[3]
{
{"Ima Wolfe", 2400.0},
{"Ura Foxe", 1300.0},
{"Iby Stout", 1800.0}
};
double * pd[3];
for (int i = 0; i < 3; i++)
pd[i] = &mr_E[i].amount;
cout << "Listing Mr. E's things:\n";
ShowArray(things, 6); //use template A
cout << "Listing Mr. E's debts:\n";
ShowArray(pd, 3); //pd is an array of pointers to double
system("pause");
return 0;
}
template <typename T>
void ShowArray(T arr[], int n)
{
using namespace std;
cout << "template A\n";
for (int i = 0; i < n; i++)
cout << arr[i] << ' ';
cout << endl;
}
template <typename T>
void ShowArray(T *arr[], int n)
{
using namespace std;
cout << "template B\n";
for (int i = 0; i < n; i++)
cout << *arr[i] << ' ';
cout << endl;
}
如果将模板B去掉,将使用A来显示pd的内容,显示的将是地址,而不是值:
重载解析寻找最匹配的函数。
如果只存在一个,则选择它;
如果存在多个,但只有一个是非模板函数,则选择该函数;
如果存在多个适合的函数,且它们都为模板函数,但其中有一个更具体,则选择该函数;
如果有多个同样适合的函数,但没有一个比其他更具体,将会调用出错;
如果不存在匹配的函数,则也会出错。
在有些情况下,可以通过合适的函数调用 ,引导编译器做出希望的选择:
程序8.15
#include <iostream>
template <class T>
T lesser(T a, T b) //#1
{
return a < b ? a : b;
}
int lesser(int a, int b) //#2
{
a = a < 0 ? -a : a;
b = b < 0 ? -b : b;
return a < b ? a : b;
}
int main()
{
using namespace std;
int m = 20;
int n = -30;
double x = 15.5;
double y = 25.9;
cout << lesser(m, n) << endl; //use #2
cout << lesser(x, y) << endl; //use #1
cout << lesser<>(m, n) << endl; //use #1
cout << lesser<int>(x, y) << endl; //use #1
system("pause");
return 0;
}
关键字decltype(C++11)
int x;
decltype(x) y; //使y和x的类型一样
decltype的参数可以是表达式:
template <class T1, class T2>
void ft(T1 x, T2 y)
{
...
decltype(x+y) xpy = x + y;
...
}
decltype(expression) var;
遍历过程:
- 如果expression是一个没有用括号括起的标识符,则var的类型与该标识符相同,包括const等;
- 如果expression是一个函数调用,则var的类型与函数的返回类型相同;(并不会实际调用函数,只是查看返回类型)
- 如果expression是一个左值,则var为指向其类型的引用;
- 如果前面的条件都不满足,则var的类型与expression的类型相同。
如果需要多次声明,可结合使用typedef和decltype:
template <class T1, class T2>
{
typedef decltype(x+y) xytype;
xytype xpy = x+y;
xytype arr[10];
xytype & rxy = arr[2];
...
}
后置返回类型
有一个问题是decltype无法解决的:
template <class T1, class T2>
?type? gt(T1 x, T2 y)
{
return x + y;
}
无法预先知道x+y的类型,为此,C++新增了一种声明和定义函数的语法。
对与下面的原型:
double h(int x, float y);
使用新增的语法可编写为:
auto h(int x, float y) -> double;
这将返回类型移到了参数生命的后面,->double被称为后置返回类型。这种语法也可用于函数定义:
auto h(int x, float y) -> double
{/*function body*/}
结合使用decltype,便可给gt()指定返回类型:
template <class T1, class T2>
auto gt(T1 x, T2 y) ->decltype(x + y)
{
return x + y;
}