C++ Primer Plus笔记07
函数模板
函数模板是统一通用的函数描述,也就是说,其中的泛型可用具体的类型替换。
也就是,编译器允许以泛型(而不是具体类型)的方式来编写程序。
假设定义了一个交换两个int值的函数,假设要交换两个double值,可以复制原本的代码,并用所有的double替换int,但这种方法麻烦且容易出错。用函数模板可以自动完成这一过程。
示例
template <typename AnyType>
void Swap(AnyType &a, AnyType &b)
{
AnyType temp;
temp = a;
a = b;
b = temp;
}
第一行:指出要建立一个模板,并将类型命名为AnyType但一般为了省事可以直接命名为T。
关键字template和typename是必需的,typename可以和class互换。
必须使用尖括号。
模板并不创建任何函数,只是告诉编译器如何定义函数。
重载模板
需要对多种类型使用相同算法的函数时,可使用模板。
使用不同算法时需要重载模板。
重载模板时,和重载一般函数一样,需要函数特征标不同。
如:
template <typename T>
void Swap(T &a, T &b); //原来的模板
template <typename T>
void Swap(T *a, T *b, int c); //新的模板,并不是所有特征标都要说泛型的
显式具体化
定义了如下结构:
struct job
{
char name[40];
double salary;
int floor;
};
另外,假设希望能够交换两个这种结构的内容。
temp = a;
a = b;
b = temp;
C++允许将结构赋给结构。然而,假设只想交换salary和floor成员,则需要不同的代码。而Swap的参数仍是两个job结构的引用,因此无法使用模板重载来提供其他的代码。
此时可以使用显式具体化
第三代具体化(ISO/ANSI C++标准)
1.对于给定的函数名,可以有非模板函数、模板函数和显式具体化模板函数以及他们的重载版本。
2.显式具体化的原型和定义应以template<>打头,并通过名称来指出类型。
3.具体化优先于常规模板,而非模板函数优于具体化和常规函数。
例:
//非模板函数原型
void Swap(job &,job &);
//模板函数原型
template <typename T>
void Swap(T &, T &);
//显式具体化模板函数原型
template <> void Swap<job>(job &,job &);
其实显式具体化是什么意思呢?
就是当你的模板函数的功能无法实现特定类型的使用时,可以让编译器特定地实现某一个特定类型的方法。
...
struct job
{
char name[40];
double salary;
int floor;
};
template <class T> //#1
void Swap(T &,T &);
template <> void Swap<job>(job &,job &); //#2
int main()
{
double u,v;
...
Swap(u,v); //用#1
job a,b;
...
Swap(a,b); //用#2
}
其中显式具体化的原型还可以这么编写:template <> void Swap(job &, job &); //省略<job>
实例化与具体化
编译器使用模板为特定类型生成函数定义时,得到的是模板实例(instantiation)。函数调用Swap(i,j);就会生成Swap()的一个实例,该实例使用了int类型。模板并非函数定义,但使用int的模板实例时函数定义。这种实例化方法被称为隐式实例化(implicit instantiation),因为程序调用Swap()函数时提供了int参数。
C++还允许显式实例化(explicit instantiation)。这意味着可以直接命令编译器创建特定的实例,如Swap<int>();
原型为:template void Swap<int>(int, int); //显式实例化
编译器看到这种声明后,将使用Swap()模板生成一个使用int类型的实例。也就是说:“使用Swap()模板生成int类型的函数定义。”
原型为:template <> void Swap<int>(int, int); //显式具体化
编译器看到这种声明后,翻译说:“不要使用Swap()模板生成int类型的函数定义,而是专门为int类型显式地定义的函数定义”
template <class T> //函数模板
void Swap(T &,T &);
template <> void Swap<int>(int &, int &); //显式具体化
template <> void Swap(int &, int &); //显式具体化
template void Swap<int>(int, int); //显式实例化
例:
template <class T>
T Add(T a, T b)
{
return a+b;
}
...
int m = 6;
double x = 10.2;
cout << Add<double>(x, m) << endl; //显式实例化
可以发现上面的x,m与模板不匹配,因为需要类型一样的参数,但使用函数模板实例化成double后,强行转换类型成double类型的实例,返回一个double类型的返回值。
总结
隐式实例化、显式实例化和显式具体化统称为具体化(specialization)。表示的都是使用具体类型的函数定义而不是通用描述。
...
template <typename T>
void Swap (T &,T &); //函数模板,template prototype
template <> void Swap<job>(job &, job &); //函数模板具体化,explicit specialization for job
int main()
{
template void Swap<char>(char &,char &); //函数模板显式实例化,explicit instantiation for char
short a,b;
...
Swap(a,b); //函数模板隐式实例化,implicit template instantiation for short
job n,m;
...
Swap(n,m); //函数模板显式具体化,,explicit specialization for job
char g,h;
...
Swap(g,h); //函数模板显式实例化,explicit instantiation for char
}
编译器选择使用哪个函数版本
对于函数重载、函数模板和函数模板重载,需要决定一个良好的策略来决定为函数调用使用哪一个函数定义。这个过程叫函数解析。
1.创建候选函数列表。包含名称相同的所有函数和模板函数。
2.使用候选函数列表创建可行列表,参数的数目正确,有些可以被强制转换类型。
3.确定是否有最佳的可行函数。没有则函数调用出错。
例:
//调用以下函数:
may('B');
//有以下函数原型:
void may(int); //#1
void 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.用户定义的转换,如类声明中定义的转换
在例子中,#1优于#2,因为char到int的转换是提升转换,而char到float的转换是标准转换。#3,#5,#6优于#1,#2,因为他们都是完全匹配的。#3优于#5和#6,因为#6是模板。
完全匹配
例:
struct blot
{int a;
char b[10];
};
blot ink = {25,"sport"};
...
//以下都是完全匹配的:
void recycle(blot);
void recycle(const blot);
void recycle(blot &);
void recycle(const blot &);
较具体的模板函数优先:显式具体化优于模板隐式生成的具体化。
非模板函数优于模板函数。
重载解析优先级总结
如果只存在一个匹配的函数,则选择他;如果存在多个,但其中只有一个是非模板函数,则选择非模板函数;如果都为模板,但有一个更具体 ,则选择更具体的;如果都合适,则会出现错误。
是什么类型
在编写模板函数时,一个问题是并非总能知道应在声明中使用哪种类型。
template<class T1, class T2>
void ft(T1 x, T2 y)
{
...
?type? xpy = x + y;
...
}
xpy到底是什么类型呢?
如果一个是int另一个是double,和应该是double。
如果一个是short另一个是int,和应该是int。
如果一个是short另一个是char,和应该是int。
关键字decltype
上面的情况可以用这样的关键字:
int x;
decltype(x) y; //让y的类型和x一样
或者:
decltype(x + y) xpy; //让xpy的类型和x+y的类型一样
xpy= x + y;
如果参数是一个左值,则var为指向其类型的引用。
double xx = 4.4;
decltype((xx)) r2 = xx; //r2是 double &
decltype(xx) w = xx; //w是double
如果需要多次声明,可结合typedef和decltype:
template <class T1, class T2>
void ft(T1 x, T2 y)
{
...
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,所以不能使用decltype。
新的使用方法为:
double h(int x, float y); //prototype
auto h(int x, float y) -> double; //new
这将返回类型移到了参数声明后面。
->double被称为后置返回类型。结合decltype就能解决上述问题:
template <class T1, class T2>
?type? gt(T1 x, T2 y) ->decltype(x + y)
{
...
return x + y;
}
总结
1.通过使用inline关键用于函数定义,并在首次调用该函数前提供其函数定义,可以使得C++编译器将该函数视为内联函数。也就是说,编译器不是让程序跳到独立的代码段来执行程序,而是用相应的代码替换函数调用。只有函数很小且重复时才使用,占用内存较多。
2.引用变量是一种伪装指针,他允许变量创建别名。主要被用于处理结构和类对象的函数参数。基类引用可以指向派生类。
3.C++可以定义默认值,如果省略了相应函数则使用默认值。默认值在函数声明时设定。