函数模板
函数模板允许以任意类型的方式来定义函数, 例如:可以这样建立一个交换模板(交换两个参数的数值)
// 建立一个模板, 并将类型命名为AnyType, 关键字template是必须的
// 类型名AnyType可以任意选择, 只要遵守C++命名规则即可. 例如T.
// typename也是必须的, 但是可以用class进行替换
template <typename AnyType>
// 定义一个模板函数
void swap(AnyType & a, AnyType & b)
{
AnyType temp;
temp = a;
a = b;
b = temp;
}
模板并不创建任何函数, 只是告诉编译器如何定义函数. 需要交换int的函数时, 编译器将按模板模式创建这样的函数, 并用int代替AnyType.
看一个简单的例子:
// 模板函数
#include <iostream>
using namespace std;
// 定义模板函数原型
// 将类型定义为T
template <typename T>
void Swap(T & a, T & b);
int main()
{
int i = 10;
int j = 20;
cout << "i = " << i << ", j = " << j << endl;
cout << "using compiler-generated int swapper: " << endl;
Swap(i, j);
cout << "Now i = " << i << ", j = " << j << endl;
double x = 1.23;
double y = 2.34;
cout << "x = " << x << ", y = " << y << endl;
cout << "using compiler-generated int swapper: " << endl;
Swap(x, y);
cout << "Now x = " << x << ", y = " << y << endl;
return 0;
}
template <typename T>
// 定义一个模板函数
void Swap(T & a, T & b)
{
T temp;
temp = a;
a = b;
b = temp;
}
程序运行结果:
注意: 函数模板不能缩短可执行程序, 对于上面的demo, 最终仍将由两个独立的函数定义, 最终的代码不包含任何模板, 而至包含了为程序生成的实际函数. 使用模板的好处是, 使得生成多个函数定义更简单, 更可靠. 通常将模板放到头文件中, 并在需要使用模板的文件中包含头文件.
模板重载
模板重载也是要根据参数列表进行重载, 并且模板函数的参数列表不一定都是泛型.
看一个demo:
// 模板函数
#include <iostream>
using namespace std;
// 定义一个模板原型
template <typename T>
// 这里是由于要交换内容, 所以使用引用变量
void Swap(T& a, T& b);
// 模板原型, 和上面的Swap进行了重载, 这里因为是要接收数组所以使用的是指针
template <class T>
void Swap(T* a, T* b, int n);
void Show(int a[]);
const int Lim = 7;
int main()
{
int i = 10, j = 20;
cout << "i = " << i << ", j = " << j << endl;
cout << "using compiler-generated int swapper: " << endl;
Swap(i, j);
cout << "Now i = " << i << ", j = " << j << endl;
int a1[Lim] = {1, 2, 3, 4, 5, 6, 7};
int a2[Lim] = {11, 22, 33, 44, 55, 66, 77};
cout << "Original arrays : " << endl;
Show(a1);
Show(a2);
Swap(a1, a2, Lim);
cout << "Swap arrays" << endl;
Show(a1);
Show(a2);
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++)
{
// a[i]也可以
temp = *(a + i);
a[i] = b[i];
b[i] = temp;
}
}
void Show(int a[])
{
for(int i = 0; i < Lim; i++)
{
cout << a[i] << ", ";
}
cout << endl;
}
程序运行结果:
为特定类型提供具体化的模板定义
显示具体化
假设定义了如下结构:
struct jbo
{
char name[40];
double salary;
int floor;
}
假设需要完成如下的功能: 交换两个结构体的salary和floor成员, 而不交换name成员, 但Swap()的参数将保持不变 , 因此无法使用模板重载来提供其他代码.
因此C++提供了一个具体化函数定义--显示具体化(explicit specialization), 其中包含所需的代码, 当编辑器找到与函数调用匹配的具体化定义时, 将使用该定义, 而不再寻找模板:
具体化方法:
1.对于给定的函数名, 可以有非模板函数, 模板函数和显示具体化模板函数以及它们的重载版本
2.显示具体化的原型和定义应该以template<>打头, 并通过名称来指出类型.
3.具体化优先于常规模板, 而非模板函数优先于具体化和常规模板.
下面是用于交换job结构的非模板函数, 模板函数和具体化的原型.
// 非模板函数, 原型可以不写变量名
void Swap(job &, job &);
// 模板原型T和&之间的空格可选
template <typename T>
void Swap(T&, T &);
// 具体化原型
// Swap<job>中的job是可选的, 因为函数的参数类型表明, 这是job的一个具体化
template<> void Swap<job>(job &, job &);
正如上面说的, 如果有多个原型, 则编译器在选择原型时, 非模板版本最高, 其次是显示具体化, 最后是模板原型.
看一个使用模版先后顺序的demo:
// 具体化模板
#include <iostream>
using namespace std;
// 普通模板原型
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& j);
int main()
{
// 规定了精确小数点后两位
cout.precision(2);
cout.setf(ios::fixed, ios::floatfield);
int i = 10, j = 20;
cout << "i = " << i << ", j = " << j << endl;
cout << "Using compiler generated int swapper:" << endl;
// 使用的是普通模板函数
Swap(i, j);
cout << "Now i = " << i << ", j = " << j << endl;
job sue = {"Tom", 120000.23, 7};
job sideny = {"Jack", 230000.45, 9};
cout << "Befor job swapping" << endl;
Show(sue);
Show(sideny);
// 由于sue和sideny是job类型的数据结构
// 因此调用的是具体化模板
Swap(sue, sideny);
cout << "After job swapping" << endl;
Show(sue);
Show(sideny);
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;
t2 = j1.floor;
j1.salary = j2.salary;
j1.floor = j2.floor;
j2.salary = t1;
j2.floor = t2;
}
void Show(job& j)
{
cout << j.name << ", salary = " << j.salary << ", floor = " << j.floor << endl;
}
程序运行结果:
实例化和具体化
记住在代码中包含函数模板本身并不会生成函数定义, 它只是一个用于生成函数定义的方案. 编译器使用模板为特定类型生成函数是由定义时, 得到的是模板实例(instantiation). 例如:上面的例子当中, 函数调用Swap(i, j)导致编译器生成Swap()的一个实例, 该实例使用int类型. 模板并非函数定义, 但是使用int的模板实例是函数定义. 这种实例化方式被称为隐式实例化(implicit instantiation), 因为编译器之所以知道需要进行定义, 是由于程序调用Swap()函数时提供了int参数.
对应的显示实例化的语法是:
// 显示实例化
template void Swap<int>(int , int);
注意和具体化的区别:
// 具体化
template<> void Swap<int>(int &, int &);
他们的区别就是具体化声明需要在关键字template后包括<>, 而显示实例化则没有.
警告:试图在同一个文件(或转换单元)中使用同一种类型的显示实例和显示具体化将出错.
还可以通过在程序中使用函数来创建显示实例化, 例如:
template <class T>
T Add(T a, T b)
{
return a + b;
}
...
int m = 6;
double x = 10.2;
// 显示实例化, 这里m会强制转换成double类型
cout << Add<double>(m, x) << endl;
如果对Swap做类似的处理:
int m = 6;
double x = 10.2;
Swap<double>(m, x);
这样也会为double生成一个显示实例化, 不幸的是, 这些代码不管用, 因为第一个形参是double&, 不能指向int变量m.
这里对显示实例化和显示具体化等做一下总结:
...
// 模板原型, T和&之间的空格可选
template <class T>
void Swap(T&, T &);
// 显示具体化
template<> void Swap<job>(job &, job &);
int main(void)
{
// 为char类型显示实例化
// 编译器看到这行代码, 会使用模板定义来生成Swap()的char版本
template void Swap<char>(char&, char &);
short a, b;
...
// 隐式实例化short类型的Swap
Swap(a, b);
job j1, j2;
...
// 调用显示具体化job模板
Swap(j1, j2);
char g, h;
...
// 调用显示实例化char类型的Swap, 在第一行代码显示实例化过
Swap(g, h);
}
编译器选择使用哪个函数版本
对于函数重载, 函数模板和函数模板重载, C++提供了一个叫重载解析(overloading resolution)的规则
1.创建候选函数列表, 其中包含于被调用函数的名称相同的函数和模板函数.
2.使用候选函数列表创建可行函数列表.
3.确定是否有最佳的可行函数, 如果有, 就使用它, 否则该函数调用出错.
例如有如下的调用:
may('B');
首先编译器将寻找名称为may()的函数和函数模板, 然后寻找那些可以用一个参数调用的函数, 例如下面这些函数原型:
void may(int); // #1
// 带有一个默认参数, 所以也可以使用单个参数进行调用
float may(float, float = 3); // #2
void may(char); // #3
char* may(const char*); // #4
char may(const char &); // #5
template<class T> void may(const T &); // #6
template<class T> void may(T *); // #7
注意只考虑参数列表, 不考虑返回值, 其中的#4, #7不可行, 因为整形类型不能被隐式的转换为指针类型, 接下来要确定那个是最佳的, 按照以下顺序来确定:
1.完全匹配, 但是常规函数优先于模板
2.提升转换(例如: char和short自动转换为int, float自动转换为double);
3.标准转换*(例如: int转换为char, long转换为double)
4.用户自定义的转换.
看一个demo:
#include <iostream>
using namespace std;
// 普通模板, 显示数组, 记得要传递进来数组的长度
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()
{
int things[6] = {1, 2, 3, 4, 5, 6};
struct debts mr_E[3] =
{
{"Ima", 2400.0},
{"Ura", 1300.0},
{"Iby", 1800.0}
};
// 创建指针数组
double* pd[3];
// 将指针数组中的每个指针指向mr_E中各个元素的amount
for(int i = 0; i < 3; i++)
pd[i] = &mr_E[i].amount;
cout << "Listing Mr. E's counts of things:" << endl;
// 调用模板函数 int型的模板函数, 参数是数组的A
ShowArray(things, 6);
cout << "Listing Mr. E's debts:" << endl;
// 使用的是指针的那个模板函数B
ShowArray(pd, 3);
return 0;
}
template<typename T>
void ShowArray(T arr[], int n)
{
cout << "template A" << endl;
for(int i = 0; i < n; i++)
cout << *(arr + i) << " ";
cout << endl;
}
template<typename T>
void ShowArray(T* arr[], int n)
{
cout << "template B" << endl;
for(int i = 0; i < n; i++)
cout << *arr[i] << " ";
cout << endl;
}
程序运行结果为:
其中pd是一个double*数组的名称, 这与模板A匹配:
其中T被替换为类型double*, 这种情况下, arr[i]将显示的是pd数组的内容也就是3个地址,
同时该函数调用也与模板B匹配
这时候, T被替换为double, 而*arr[i]则显示的是数组内容指向的double值,
在这两个模板中, 模板B更具体, 因为B中指出了数组内容是指针, 因此被使用(按我的理解就是B模板的参数列表与pd更为复合).
如果将模板B从程序中删除, 则编译器会使用模板A来显示pd内容, 因此会显示的是地址而不是值
自己选择
看一个dmeo:
// 自己选择模板
#include <iostream>
using namespace std;
// 模板
template<class T>
T lesser(T a, T b)
{
return a < b ? a : b;
}
int lesser(int a, int b)
{
a = a < 0 ? -a : a;
b = b < 0 ? -b : b;
return a < b ? a : b;
}
int main()
{
int m = 20;
int n = -30;
double x = 12.34;
double y = 56.78;
// 优先使用普通方法
cout << lesser(m, n) << endl;
// 使用double类型的模板方法
cout << lesser(x, y) << endl;
// lesser<>指明了要使用模板方法, 因为参数是int型的, 所以使用的是int型模板方法
cout << lesser<>(m, n) << endl;
// 强制使用模板方法, 会把x, y先转化成int型然后使用
cout << lesser<int>(x, y) <<endl;
}
运行结果:
程序中值得注意的地方都在注释中写明白了
模板函数更进一步
看如下的模板:
template<class T1, class T2>
void ft(T1 x, T2 y)
{
...
?type? xpy = x + y;
...
}
这种情况下xpy应该是什么类型呢?
由于不知道ft将如何使用, 因此我们也不知道x + y是怎么类型,
在C++11中引入了关键字decltype来解决这个问题, 例如:
int x;
// 将y声明成x的类型
decltype (x) y;
因此上面例子中的代码就可以写成:
decltype(x + y) xpy;
xpy = x + y;
或者也可以合并成一句:
// 定义xpy为 x + y的类型
decltype(x + y) xpy;
因此前面的那个模板函数可以写成:
template<class T1, class T2>
void ft(T1 x, T2 y)
{
...
decltype(x + y) xpy = x + y;
...
}
decltype为确定类型, 编译器会遍历一个核对表, 例如有如下声明:
decltype(expression) var;
1.如果expression是一个没有用()括起来的标识符, 则var类型与该标识符相同, 包括const等限定符:
double x = 5.5;
doubel y = 7.7;
double & rx = x;
const double* pd;
// w是double类型
decltype(x) w;
// u是double& 类型, 也就是double的引用类型
decltype(rx) u = y;
// v是double指针类型
decltype(pd) v;
2.如果expression是一个函数调用, 则var的类型与函数的返回类型相同;
// 函数原型
long indeed(int);
// m是long类型
decltype(indeed(3)) m;
3.如果expression是一个用括号括起来的标识符, 则表示引用
double xx = 3.3;
// w是一个double
decltype(xx) w = xx;
// r2是一个double&
decltype((xx)) r2 = xx;
4.如果前面的条件都不满足, 则var的类型与expression的类型相同:
int j = 3;
int& k = j;
int& n = j;
// i1是int型
decltype(j + 6) i1;
// i2是long型
decltype(100L) i2;
// i3是int型, 虽然k, n都是引用, 但是k + n不是引用, 而是两个int的和
decltype(k + n) i3;
如果需要多次声明, 可结合typedef和decltype一起使用
template<class T1, class T2>
void ft(T1 x, T2 y)
{
...
typedef decltype(x + y) xpytype;
xpytype xpy = x + y;
xpytype arr[10];
xpytype& rxy = arr[2];
...
}
另一种函数声明语法(C++后置返回类型)
template<class T1, class T2>
?type? get(T1 x, T2 y)
{
...
return x + y;
}
这种情况, 好像可以将返回类型设置为decltype(x + y), 但不行的是, 此时还未声明参数x和y, 他们不在作用于内. 必须在声明参数后使用decltype.
为此C++新增了一种声明和定义函数的语法:
对于下面的原型:
double h(int x, float y);
使用新增的语法可编写成这样:
auto h(int x, float y) -> double;
这将返回类型移到了参数声明后面. ->double被称为后置返回类型(trailing return type). 其中auto是一个占位符, 表示后置返回类型提供的类型. 这种语法也可以用于函数定义:
auto h(int x, float y) -> double
{
...
}
通过结合使用这种语法和decltype, 便可给gt()指定返回类型, 如下:
template<class T1, class T2>
auto gt(T1 x, T2 y) -> decltype(x + y)
{
...
return x + y;
}