在现实生活中,为了提高工作效率,我们通常会用一些模板来完成一些事情,比如:年终总结的ppt,我们可以拿一些现成的ppt模板来写,通常我们只需将内容进行填充就可以了,而不需要从0到1的去制作ppt的各个方面,我们只需要关注最核心的地方(对我们而言,年终总结的内容才是最关键的)就可以了。
而在C++中,模板也是同样的道理,它的出现,大大提高了代码的复用性,使我们的代码更具通用性,更灵活!C++最重要的特性之一就是,代码的重用,为了实现代码重用,代码就必须具有通用性。通用代码不受数据类型的影响,并且可以自动适应数据类型的变化,这种程序设计类型称为参数化程序设计。
模板是C++支持参数化程序设计的工具,通过它可以实现参数化多态性。所谓参数化多态性是指:将程序所处理的对象的类型参数化,使得一段程序可以处理多种不同类型的对象。简单的理解,模板就是为了让代码的通用性更强。
有了以上的理解, 下面理解函数模板和类模板就轻松多了。
要讲函数模板,就得先讲一讲函数重载,相信大家都知道,函数重载通常是对于不同的数据类型完成类似的操作。
在很多时候,一个算法其实是可以处理多种数据类型的。但是用函数实现算法时,即使设计为重载函数也只是
使用相同的函数名,函数体仍然要分别定义。下面举一个求绝对值的函数重载的实例,方便大家理解。
int abs(int x){
return x<0?-x:x;
}
double abs(double x){
return x<0?-x:x;
}
可以看出,这两个函数的功能和函数体是一模一样的,但是为了实现对不同数据类型的数据求绝对值操作,我们不得不写两个同名函数来实现操作。
如果能写一段通用代码适用与多种不同数据类型该多好呢,这会使代码的可重用性大大提高,从而提高软件的开发效率。所以便设计出了这么一个工具,那就是函数模板,程序员只需对函数模板编写一次,然后基于调用函数时提供的参数类型,C++编译器将自动产生相应的函数来正确处理该类型的数据。
函数模板的定义形式如下:
template
类型名 函数名(参数列表)
{
函数体的定义;
}
上述typename标识符是后来新出的标识符,更容易理解,之前的C++版本也可以使用class标识符替换这里的typename,该标识符指明可以接收一个类型参数,这些类型参数代表的是类型,可以是系统预定义的数据类型,也可以是自定义类型。类型参数可以用来指定函数模板本身的形参类型、返回值类型,以及声明函数中的局部变量。
例如,上面的函数可以用函数模板替换成:
template
T abs(T x){
return x<0?-x:x;
}
函数模板并不是一个真正的函数,它只是一段编译指令,在你的程序下文调用某个类型的函数时,它才会真正的展开成一个这种类型的函数。
听起来可能有点抽象,不过这一点,我们可以通过观察汇编代码去较为清晰的证明。(这在里推荐一个比较好用的实时看汇编代码的网站:Compiler Explorer (godbolt.org))
首先,我们定义一个普通函数:
int abs(int x){
return x<0?-x:x;
}
使用刚刚的网站,看一看它的汇编代码:
然后,我们定义一个函数模板:
template
T abs(T x){
return x<0?-x:x;
}
再次使用刚刚的网站,看一看它的汇编代码:
你会发现其对应的汇编代码是空的!而当我们在下面调用某种类型的函数时:
看见了吗?这里它生成了两种类型的abs函数,这是因为我们在主函数里调用了这两种类型的函数,也就是说,只有当我们实际调用的时候,这种类型的函数才会通过函数模板这一“蓝图”生成。只写一个模板函数而不去调用就什么函数也不会生成!
在上面我们这样调用了abs函数:
abs(1);
abs(1.0);
这种是通过显式调用,还有一种更方便的隐式调用:
abs(1);
abs(1.0);
在这种情况下,编译器会根据形参类型自动推导需要生成什么类型的abs函数。
模板就像是你在C++里雇佣的一个员工,你命令它,它帮你写代码。当然前提是你的命令得正确。
abs这个函数比较简单,下面我们再来看一个函数:
template
T add(T x,T y){
return x+y;
}
如果我们这样使用它:
cout<
如何解决这种办法?
首先,我们可以显示的调用add函数:
cout<(1,2.9)<
这时,形参会被强制转换成int类型,从而正常调用add函数。
还有一种更好的办法:
template
T add(T x,U y){
return x+y;
}
我们可以定义两种参数,但这个时候问题又来了:我们可以区分x和y的类型,但是x+y的类型呢?也就是说返回值类型如何确定?这里介绍一个关键字:decltype.
decltype是C++11新增的一个关键字,和auto的功能一样,用来在编译时期进行自动类型推导。引入decltype是因为auto并不适用于所有的自动类型推导场景,在某些特殊情况下auto用起来很不方便,甚至压根无法使用。
auto varName=value;
decltype(exp) varName=value;
auto根据=右边的初始值推导出变量的类型,decltype根据exp表达式推导出变量的类型,跟=右边的value没有关系;
auto要求变量必须初始化,这是因为auto根据变量的初始值来推导变量类型的,如果不初始化,变量的类型也就无法推导,而decltype不要求,因此可以写成如下形式
decltype(exp) varName;
原则上将,exp只是一个普通的表达式,它可以是任意复杂的形式,但必须保证exp的结果是有类型的,不能是void;如exp为一个返回值为void的函数时,exp的结果也是void类型,此时会导致编译错误
int x = 0;
decltype(x) y = 1; // y -> int
decltype(x + y) z = 0; // z -> int
const int& i = x;
decltype(i) j = y; // j -> const int &
const decltype(z) * p = &z; // *p -> const int, p -> const int *
decltype(z) * pi = &z; // *pi -> int , pi -> int *
decltype(pi)* pp = π // *pp -> int * , pp -> int * *
左值:表达式执行结束后依然存在的数据,即持久性数据;右值是指那些在表达式执行结束不再存在的数据,即临时性数据。
一个区分的简单方法是:对表达式取地址,如果编译器不报错就是左值,否则为右值
template
class A
{
private :
decltype(T.begin()) m_it;
//typename T::iterator m_it; //这种用法会出错
public:
void func(T& container)
{
m_it=container.begin();
}
};
int main()
{
const vector v;
A> obj;
obj.func(v);
return 0;
}
现在有了这个关键字,你可能会说这时候函数模板可以写成这样:
template
decltype(x+y) add(T x,U y){
return x+y;
}
但是你有没有想过,当编译器执行到decltype的时候,还并不知道(x+y)是个什么类型,自然也就无法进行推导。
所以应该怎么写呢?这里给出一种方案:利用构造函数:
template
decltype(T()+U()) add(T x,U y){
return x+y;
}
利用这种当时就可以实现不同类型的两个数相加。
这时候你可能会说,那如果T和U没有默认构造函数怎么办?像这种情况:
#include
using namespace std;
class A{
int x;
public:
A() = delete;
A(int x):x(x){}
A operator+(const A& b){
return (x+b.x);
}
friend ostream& operator<<(ostream & os , const A& a){
return os<
decltype(T()+U()) add(T x,U y){
return x+y;
}
int main(){
A aa(3),bb(4);
cout<
这种情况又该怎么办呢?
关于函数的返回值,其实C++提供了两种方式,我们常用的只是其中一种:
int getSum(int a, int b);
还有一种方式叫返回类型后置
auto getSum(int a, int b)->int;
那时候后面这种返回类型后置的用法就用上了:
#include
using namespace std;
class A{
int x;
public:
A() = delete;
A(int x):x(x){}
A operator+(const A& b){
return (x+b.x);
}
friend ostream& operator<<(ostream & os , const A& a){
return os<
auto add(T x,U y)->decltype(x+y)
{
return x+y;
}
int main(){
A aa(3),bb(4);
cout<
前面我们讨论过函数模板存在的意义,很显然,我们可以通过合理的使用函数模板实现泛型编程,也就是说定义一个函数模板,然后理想情况下支持任何形式的调用。但是事实上,即便我们定义的函数模板在大多数情况下能够满足我们的需求,然而总有一些特殊情况需要我们特殊处理,于是便有了隐式实例化,显式实例化,部分具体化,显式具体化。这些统称为函数具体化。
我们前面提到的两种实例化的方式都是属于隐式实例化,这种实例化的特点是只有我们真正需要一个函数时,函数模板才会为我们实例化这种类型的函数。
相较于前面的隐式实例化,如果当使用者确认某种类型的函数在接下来一定会被使用的时候,我们可以为其显式实例化,相关语法为:
template void Swap(int,int);//显式实例化
编译器看到上述声明后,将使用Swap( )模板生成一个使用int类型的实例。也就是说,该声明的意思是“使用Swap( )模板生成int类型的函数定义。”。这种实例化的好处是节约了在编译器生成这种函数的时间。
对于刚刚的函数模板:
template
T abs(T x){
return x<0?-x:x;
}
有时候可能会要求针对与某一种类型进行特殊处理,例如不仅仅是返回,还需要进行打印输出,这时候就需要针对这种情形进行“特化”:
template <>
T abs(int x){//这里可以省略
cout<
void myPrint(int a, int b)
{
cout << "调用的普通函数" << endl;
}
template
void myPrint(T a, T b)
{
cout << "调用的是函数模板" << endl;
}
void Mytest()
{
int a = 10;
int b = 20;
myPrint(a, b);
}
int main()
{
Mytest();
return 0;
}
//输出:调用的普通函数
void myPrint(int a, int b)
{
cout << "调用的普通函数" << endl;
}
template
void myPrint(T a, T b)
{
cout << "调用的是函数模板" << endl;
}
void Mytest()
{
int a = 10;
int b = 20;
myPrint<>(a, b); //通过空模板参数列表来强制调用函数模板
}
int main()
{
Mytest();
return 0;
}
//输出:调用的是函数模板
函数模板也可以发生重载;
template
void myPrint(T a, T b)
{
cout << "调用的是函数模板" << endl;
}
template
void myPrint(T a, T b,T c)
{
cout << "调用的是重载的函数模板" << endl;
}
void Mytest()
{
int a = 10;
int b = 20;
myPrint(a, b, 100);
}
int main()
{
Mytest();
return 0;
}
//输出:调用的是重载的函数模板
如果函数模板可以产生更好的匹配,优先调用函数模板;
void myPrint(int a, int b)
{
cout << "调用的普通函数" << endl;
}
template
void myPrint(T a, T b)
{
cout << "调用的是函数模板" << endl;
}
void Mytest()
{
char c1 = 'a';
char c2 = 'b';
myPrint(c1, c2);
}
int main()
{
Mytest();
return 0;
}
//调用的是函数模板
模板的重载和嵌套使用;
#include
using namespace std;
template
T Max(T a,T b)
{
return((a > b)?a : b);
}
template
T Max(T a,T b,T c)
{
return Max(Max(a,b),c); //嵌套
}
template
T Max(T a,T b,T c,T d)
{
return Max(Max(a,b,c),d); //嵌套
}
int main(int argc, char *argv[])
{
cout << Max(8,11) << endl;
cout << Max(4,8,9) << endl;
cout << Max(4.2,2.8,5.9) << endl;
cout << Max(5.4,3.8,2.9) << endl;
cout << Max(5.4,3.8,2.9,8.1) << endl;
return 0;
}
//输出结果
11
9
5.9
5.4
8.1
--------------------------------
Process exited after 0.08955 seconds with return value 0
请按任意键继续. .
模板是一个通用框架,是C++泛型编程思想的主要体现。普通函数与函数模板的区别主要如下:
int myAdd1(int a, int b) //普通函数
{
return a + b;
}
template
int myAdd2(T a, T b) //函数模板
{
return a + b;
}
void Mytest()
{
int a = 10;
int b = 20;
char c = 'c';
cout << myAdd1(a, c) << endl;
//隐式类型推导
cout << myAdd2(a, b) << endl;
//cout << myAdd2(a, c) << endl;会报错,T的类型不一致
//显示指定类型
myAdd2(a, c); //不会报错,会发生隐式类型转换
}
int main()
{
Mytest();
return 0;
}