纵有疾风起,人生不言弃。本文篇幅较长,如有错误请不吝赐教,感谢支持。
💬文章目录
- 一.模板的基础知识
- ①为什么有模板?
- ②初识模板
- 二.函数模板
- ①函数模板的定义
- ②函数模板的使用
- ③函数模板实例化
- 1️⃣隐式实例化
- 2️⃣显示实例化
- ④函数模板的具体化(特化、特例化)
- 1️⃣模板的局限性及解决方法
- 2️⃣函数模板具体化(特化、特例化)
- 3️⃣实例化和具体化的相同点
- ⑤普通函数和函数模板调用规则
- ⑥函数模板的注意事项
- ⑦函数模板的分文件编写
- 三.函数模板高级
- ①decltype关键字
一.模板的基础知识
①为什么有模板?
在C++程序中,声明变量、函数、对象等实体时,程序设计者需要指定数据类型,让编译器在程序运行之前进行类型检查并分配内存,以提高程序运行的安全性和效率。但是这种强类型的编程方式往往会导致程序设计者为逻辑结构相同而具体数据类型不同的对象编写模式一致的代码。
🃏案例:
//交换int数据
void SwapInt(int& a,int& b)
{
int temp = a;
a = b;
b = temp;
}
//交换char数据
void SwapChar(char& a,char& b)
{
char temp = a;
a = b;
b = temp;
}
问题:如果我要交换double类型数据,那么还需要些一个double类型数据交换的函数,繁琐,写的函数越多,当交换逻辑发生变化的时候,所有的函数都需要修改,无形当中增加了代码的维护难度,如果能把类型作为参数传递进来就好了,传递int就是int类型交换,传递double类型就是double类型交换。
②初识模板
C++标准提供了模板机制,用于定义数据类型不同但逻辑结构相同的数据对象的通用行为。在模板中,运算对象的类型不是实际的数据类型,而是一种参数化的类型。带参数类型的函数称为函数模板,带参数类型的类称为类模板。
例如上面的案例,我们可以定义函数Swap(),交换两个参数,我们可以将类型参数化,如Swap(T,T),其中,T就是参数化的类型,在调用Swap()函数时,可以传入任意类型的数据,函数可以根据传入的数据推导出T的值是哪种数据类型,从而进行相应的操作。这样程序设计者就可以专注于逻辑代码的编写,而不用关心实际具体的数据类型。
模板就像生产模具,例如,中秋生产月饼,生产月饼的模具就是模板,在做模具时,只关心做出什么样式的月饼,而不用关心月饼具体的原料是什么(如水果、五仁、豆沙等)。
程序运行时,模板的参数由实际参数的数据类型决定,编译器会根据实际参数的数据类型生成相应的一段可运行代码,这个过程称为模板实例化。函数模板生成的实例称为模板函数,类模板生成的实例称为模板类。
二.函数模板
函数模板是函数的抽象,它与普通函数相似,唯一的区别就是函数参数的类型是不确定的,函数参数的类型只有在调用过程中才被确定。
①函数模板的定义
C++提供了函数模板(function template.)所谓函数模板,实际上是建立一个通用函数,其函数类型和形参类型不具体制定(泛型),用一个虚拟的类型来代表。这个通用函数就成为函数模板。凡是函数体相同的函数都可以用这个模板代替,不必定义多个函数,只需在模板中定义一次即可。 在调用函数时系统会根据实参的类型来取代模板中的虚拟类型,从而实现不同函数的功能。
定义函数模板的语法格式如下所示:
上述语法格式中,template是声明模板的关键字,<>中的参数称为模板参数;typename关键字用于标识模板参数,可以用class关键字代替,class和typename并没有区别。
- 模板参数< >中不能为空,一个函数模板中可以有多个模板参数,模板参数和普通函数参数相似,只是不确定数据类型。
- template下面是定义的函数模板,函数模板定义方式与普通函数定义方式相同只是参数列表中的数据类型要使用<>中的模板参数名表示 。
②函数模板的使用
template<class T>//T是模板参数
void mySwap(T &a,T &b)
{
T tmp = a;
a = b;
b = tmp;
}
//使用函数模版,和正常调用函数没区别
void test()
{
int a = 10,b = 20;
double c=3.1415,d=9.26;
//1.编译器会根据实参来自动推导数据类型,并生成相应的函数
mySwap(a,b);//mySwap中的T会变成int类型
cout << "a=" << a <<"b=" << b << endl;
mySwap(c,d);//mySwap中的T会变成double类型
cout << "c=" << c << "d=" << d << endl;
}
🖲解释:
- 第一行指出,要建立一个模板,并将类型命名为T。关键字template和class是必需的,也可以使用关键字typename代替class.另外,必须使用尖括号<>。类型名可以任意选择(这里为T)。
- 余下的代码交换两个T值的算法。模板并不创建任何函数,而只是告诉编译器如何定义函数。需要交换int的函数时,编译器将按模板模式创建这样的函数,并用int代替T。同样,需要交换double的函数时,编译器将按模板模式创建这样的函数,并用double代替T.
③函数模板实例化
所谓函数模板实例化,就是用类型参数替换函数模板中的模板参数,生成具体类型的函数定义。
实例化可分为:
- 隐式实例化
- 显式实例化
下面分别介绍这两种实例化方式。
1️⃣隐式实例化
隐式实例化是根据函数调用时传入的参数的数据类型确定模板参数T的类型,模板参数的类型是隐式确定的。
例如下例调用mySwap()函数模板时,传入的是int类型数据a和b,编译器根据传入的实参推演出模板参数T的类型是int,就会根据函数模板将T全部替换成int,实例化出一个int类型的函数,如下所示:
//交换int数据
void mySwap(int& a,int& b)
{
int temp = a;
a = b;
b = temp;
}
int main()
{
int a=20,b=30;
mySwap(a,b);
return 0;
}
编译器生成具体类型函数的这一过程就称为实例化,生成的函数称为模板函数。
需要注意的是:
①不能在函数调用的参数中指定模板参数的类型,对函数模板的调用应使用实参推演。例如,只能进行Swap(2,3)这样的调用,或者定义整型变量int a=20,b=30,再将变量a、b作为参数,进行Swap(a,b)这样的调用,编译器会根据传入的实参推演出T为int类型,而不能使用Swap(int,int)方式,直接将类型传入进行调用。
②函数模板不会减少可执行程序的大小,因为编译器会根据调用时的参数类型进行相应的实例化。
2️⃣显示实例化
隐式实例化不能为同一个模板参数指定两种不同的类型。
例如:
template<class T>//T是模板参数
void mySwap(T &a,T &b)
{
T tmp = a;
a = b;
b = tmp;
}
//就一个模板参数T,你隐式的给传两个数据类型
Swap(10,3.14)//编译器搞不清楚T是什么数据类型,二义性。
//那就让我们显示的告诉它
函数参数类型不一致,编译器便会报错。这就需要显式实例化解决类型不一致的问题。显式实例化需要指定函数模板中的模板参数的数据类型,语法格式如下所示:
< >中是显式实例化的数据类型,即要实例化出一个什么类型的函数。相当于主动告诉编译器,我希望你将用我传的参数的数据类型去将模板参数显示转换为该类型,并按照模版规定的逻辑生成函数定义。
<实例化类型>尖括号中实例化类型数量要与你定义模板时定义模板参数数量一致,可以理解用你显示具体化的数据类型去替换你定义模板时模板参数。
🃏案例:
template<class T>
void mySwap(T& a, T& b)
{
T tmp = a;
a = b;
b = tmp;
}
template void mySwap<int>(int& a, int& b);//int去替换定义函数模板时的T
void test()
{
int a = 10,b= 3;
mySwap<int>(a, b);//显示告诉模板mySwap中的T会变成int类型
cout << "a=" << a << endl;
cout << "b=" << b << endl;
}
🖲代码运行结果如下:
④函数模板的具体化(特化、特例化)
1️⃣模板的局限性及解决方法
假设有如下模板函数:
template<class T>
void func(T a, T b)
{ … }
如果代码实现时定义了赋值操作 a = b,但是T为数组,这种假设就不成立了同样,如果里面的语句为判断语句 if(a>b),但T如果是结构体,该假设也不成立,另外如果是传入的数组,数组名为地址,因此它比较的是地址,而这也不是我们所希望的操作。总之,编写的模板函数很可能无法处理某些类型,另一方面,有时候通用化是有意义的,但C++语法不允许这样做。为了解决这种问题,可以提供模板的重载,为这些特定的类型提供显示具体化的模板。
2️⃣函数模板具体化(特化、特例化)
函数模板的特点是通用性,可以解决某个方面的普遍性问题,但是这个世界上的事情从来不是绝对的,有普遍的,就有特殊的。
可以提供一个具体化的函数定义,当编译器找到与函数调用匹配的具体化定义时,将使用该定义,不再寻找模板,函数模板是对函数体重新定义。
具体化(特例化、特化)的语法:
注意显示实例化的前缀是template,没有尖括号<>。
举例:交换两个学生类的成员变量。
#include <iostream>
using namespace std;
//函数模板
template<class T>
void mySwap(T &a,T &b)
{
T tmp = a;
a = b;
b = tmp;
cout<<"调用了mySwap(T &a,T &b)"<<endl;
}
class Student
{
public:
Student(int num):score(num){}
int score;
};
//具体化函数模板,为Student这一数据类型单独写一份函数定义
template <>void mySwap<Student>(Student &a,Student &b)
{
int tmp = a.score;
a.score = b.score;
b.score = tmp;
cout<<"具体化函数模板:template <>void mySwap<Student>(Student &a,Student &b)调用"<<endl;
}
int main()
{
int a = 10,b = 20;
mySwap(a,b);//调用函数模板
cout << "a=" << a << "b=" << b << endl;
Student s1(150),s2(90);
mySwap(s1,s2);//调用具体化函数模板
cout << "s1.score" << s1.score<< "s2.score" << s2.score << endl;
return 0;
}
注意:具体化函数的返回值、函数名和形参列表与函数模板相同。但是对于具体化函数模板,不管是函数声明还是定义中,都是具体的数据类型,没有通用的数据类型了。
3️⃣实例化和具体化的相同点
实例化和具体化都是用于生成具体的定义,实例化是编译器借助你提供的数据类型来隐式或显示替换模板中的模板参数来实例化出函数定义,而具体化则是你专门为一个具体的数据类型写函数定义。
例如:
显示实例化
template void Swap<int>(int&a,int&b)//显示具体化
//使用Swap()模板生成int类型的函数定义
//就是编译器借助你提供的int去显示替换模板中的模板参数
//生成int函数定义类型。
显示具体化
template<>void Swap <int>(int&a,int&b)//显示具体化
//不要使用Swap( )模板来生成函数定义
//而应使用专门为int类型显式地定义的函数定义
⑤普通函数和函数模板调用规则
- ✅如果函数有多个原型,则编译器在选择函数调用时,普通函数优先于模板函数,显式具体化模板优先于函数模板。
- ✅如果希望使用函数模板,可以用空模板参数强制使用函数模板。
- ✅如果函数模板可以产生一个更好的匹配,那么选择模板。
#include <iostream>
using namespace std;
//普通函数
void mySwap(int a, int b)
{
int tmp = a;
a = b;
b = tmp;
cout << "调用了普通函数mySwap(int a,int b)" << endl;
}
//函数模板
template<class T>
void mySwap(T& a, T& b)
{
T tmp = a;
a = b;
b = tmp;
cout << "调用了函数模板mySwap(T &a,T &b)" << endl;
}
class Student
{
public:
Student(int num) :score(num) {}
int score;
};
//显示具体化
template <>void mySwap<Student>(Student& a, Student& b)
{
int tmp = a.score;
a.score = b.score;
b.score = tmp;
cout << "具体化函数模板:template <>void mySwap<Student>(Student &a,Student &b)调用" << endl;
}
int main()
{
int a = 10, b = 20;
//如果函数模板和普通函数都能匹配,c++编译器优先考虑普通函数
mySwap(a, b);
//如果我必须要调用函数模板,那么怎么办?
//可以通过空模板实参列表的语法限定编译器只能通过模板匹配
mySwap<>(a, b);
cout << "a=" << a << "b=" << b << endl;
Student s1(150), s2(90);
//调用显示具体化函数模板
mySwap(s1, s2);
cout << "s1.score:" << s1.score << "s2.score:" << s2.score << endl;
//此时普通函数和显示具体化模板都没有很好的匹配
//所以使用函数模板可以产生一个更好的匹配,那么选择模板
char d = 'd', e = 'e';
mySwap(d, e);
return 0;
}
⑥函数模板的注意事项
1️⃣可以为类的成员函数创建模板,但不能是虚函数和析构函数。
#include <iostream>
#include<string>
using namespace std;
class Maker
{
public:
template<typename T>
Maker(T a)
{
cout<<a<<endl;
}
template<typename T>
void show(T b)
{
cout<<b<<endl;
}
};
int main()
{
Maker m1("感谢支持强风吹拂king的博客");
m1.show(666);
}
上例将Maker的构造函数和成员函数show声明成函数模板,可以接受任意的数据类型。
如果将show声明为虚函数会怎么样呢?
也不能把析构函数声明成函数模板,一个类的析构函数只有一个,根本没有参数,不需要模板。
2️⃣只要使用函数模板,就必须明确数据类型(不论是显示实例化还是隐式实例化),让编译器知道模板参数到底是个什么数据类型,确保实参与函数模板能匹配上,生成具体的函数定义。
举例:
template<class T>//T是模板参数
void mySwap(T &a,T &b)
{
T tmp = a;
a = b;
b = tmp;
}
//使用函数模版
int main()
{
int a = 10,b = 20;
mySwap(a,b);
cout << "a=" << a << endl;
cout << "b=" << b << endl;
}
现在mySwap函数描述了一种通用类型T,函数这两个形参都是T定义的,也就是说这两个形参的数据类型必须相同,在main函数中,a和b都是int类型没有任何问题。
int main()
{
int a = 10
char b = 20;
mySwap(a,b);
cout << "a=" << a << endl;
cout << "b=" << b << endl;
}
如果将b改为字符,编译器报错了,模板参数T不明确,未能找到匹配的重载函数。
实参与函数模板的匹配,和函数重载道理一致,如果匹配不上就会报错。
把函数模板的形参都删掉,函数里面的代码也删掉。现在mySwap函数没有参数,那么编译还能通过吗?
template<class T>//T是模板参数
void mySwap()
{
cout<<"show函数被调用"<<endl;
}
//使用函数模版
int main()
{
mySwap();
}
未能推导模板参数。
也就是说模板参数很死板,一定要明确数据类型,上面无参show函数,不能通过参数隐式实例化函数模板,那就只能显示实例化。
mySwap<int>();
3️⃣使用函数模板时,如果是隐式实例化函数模板,不会发生隐式类型转换,如果显式实例化函数模板,则可以发生隐式类型转换。
举例:
template<class T>//T是模板参数
T Add (T a,T b)
{
return a+b;
}
//使用函数模版
int main()
{
int a = 10,b = 20;
cout<<Add(a,b)<<endl;
}
现在a和b都是整型,把他们作为函数模板的实参,编译器推导出来的就是整型,与函数模板的定义是匹配的。
我们将b改为字符,在C++中字符是可以隐式转换成整型的。
int main()
{
int a = 10;
char b = 20;
cout<<Add(a,b)<<endl;
}
模板参数不明确,看来隐式实例化没有将字符b转换成整型。
那我们显示实例化试一下:
int main()
{
int a = 10;
char b = 'a';
cout<<Add<int>(a,b)<<endl;
}
编译成功。也就是说如果显式实例化函数模板,则可以发生隐式类型转换。
4️⃣如果函数模板有多个模板参数,则每个模板参数前都必须使用关键字class或typename修饰。
例如:
template<typename T, typename U>//两个关键字可以混
void func(T t, U u){}
template<typename T,U>//错误,每一个模板参数前都必须有关键字修饰
void func(T t, U u){}
5️⃣全局作用域中声明的与模板参数同名的对象、函数或类型,在函数模板中将被隐藏。
例如:
int num=9000;
template<typename T>
void func(T t)
{
T num;
cout<<num<<endl;//输出的是局部变量num,全局int类型的num被屏蔽
}
在函数体内访问的num是T类型的变量num,而不是全局int类型的变量num。
6️⃣函数模板中声明的对象或类型不能与模板参数同名。
例如:
template<typename T>
void func(T t) 
{
typedef float T;//错误,定义的类型与模板参数名相同
}
7️⃣模板参数名在同一模板参数列表中只能使用一次,但可在多个函数模板声明或定义之间重复使用。
例如:
template<typename T, typename T>//错误,在同一个模板中重复定义模板参数 
void func1(T x, T y){} 
template<typename T> 
void func2(T z){} 
template<typename T>//在不同函数模板中可重复使用相同的模板参数名
void func3(T w){}
⑦函数模板的分文件编写
函数模板只是函数的描述,没有实体,创建函数模板的代码放在头文件中。
函数模板的具体化有实体,编译的原理和普通函数一样,所以,声明放在头文件中,定义放在源文件中。
例如把下面这段代码分文件编写:
#include <iostream>
using namespace std;
void Swap(int a, int b) // 普通函数。
{
cout << "使用了普通函数。\n";
}
template <typename T>
void Swap(T a, T b) // 函数模板。
{
cout << "使用了函数模板。\n";
}
template <>
void Swap(double a, double b) // 函数模板的具体化版本。
{
cout << "使用了具体化的函数模板。\n";
}
int main()
{
Swap(1,2); //将使用普通函数。
Swap(1.3, 3.5); //将使用具体化的函数模板。
Swap('c', 'd'); //将使用函数模板。
}
头文件public.h,存放函数的声明
#pragma once
#include <iostream>
using namespace std;
void Swap(int a, int b); //普通函数的声明
template <typename T>
void Swap(T a, T b) // 函数模板的定义
{
cout << "使用了函数模板。\n";
}
template <>
void Swap(double a, double b); // 函数模板的具体化的声明
public.cpp,存放函数的实现
#include "public.h"
void Swap(int a, int b) //普通函数的实现
{
cout << "使用了普通函数。\n";
}
template <>
void Swap(double a, double b) // 函数模板的具体化的实现
{
cout << "使用了具体化的函数模板。\n";
}
demo01.cpp 测试文件
#include "public.h"
int main()
{
Swap(1,2); // 将使用普通函数。
Swap(1.3, 3.5); // 将使用具体化的函数模板。
Swap('c', 'd'); // 将使用函数模板。
}
三.函数模板高级
①decltype关键字
先来看一个示例:
template<typename T1,typename T2>
void func(T1 a, T2 b)
{
//其他代码
??? tmp=a+b;
//其他代码
}
func函数模板描述了两个通用类型T1和T2,形参a和b分别用T1和T2定义,在函数体内声明tmp保存a+b的结果,那么tmp怎么声明,用什么数据类型,T1还是T2,还是其他?
如果T1是double,T2是int,那么最终结果是double类型;如果T1是short,T2是int,那么最终结果是int类型;还有除了C++的内置数据类型,结构体和类,还可以重载+号运算符,这样情况就更复杂了,这样的话tmp又将定义成什么类型呢?
在C++11中,decltype操作符,用于查询表达式的数据类型。
语法:
decltype(表达式) var;
decltype分析表达式并返回它的类型,
用这个类型定义var变量。
decltype分析表达式并返回它的类型,不会计算执行表达式。函数调用也一种表达式,因此不必担心在使用decltype时执行了函数。
decltype推导规则(按步骤):
- 1)如果表达式是一个没有用括号括起来的标识符,decltype本身自带的()不算,则var的类型与该标识符的类型相同,包括const等限定符。
int main()
{
const short a = 100;
decltype(a) var=666;
//var的类型将和a保持一致。
//注意:如果decltype分析表达式的类型是引用
//那么必须初始化这个引用
short b = 10;
short& pa = b;
decltype(pa) var2 = b;//var2的类型是short&,初始化var2的引用指向b;、
cout << "var:" << var <<"var2:" << var2 << endl;
}
- 2)如果表达式是一个函数调用,则var的类型与函数的返回值类型相同(函数不能返回void,但可以返回void * ,因为void * 可以声明变量)。
int func()
{
cout<<"调用了func函数"<<endl;
return 666;
}
int main()
{
int b=100;
decltype(func()) val=b;
}
大家看,val的类型,是func函数的返回类型int。
,我们运行代码,发现并没有调用func函数,这也说明decltype不会计算执行表达式。
✅还有一点:
在decltype中填函数名和函数调用是两回事。
int main()
{
decltype(func()) val=b;
//填函数调用,得到的是函数的返回类型
decltype(func) *val2=func;
//只填函数名得到的是函数的类型
//再加*号就是函数指针类型,我们让val2指向func函数
val2();//借助函数指针val2调用func函数
}
- 3)如果表达式是一个左值(能取地址)(要排除第一种情况)、或者用括号括起来的标识符,那么var的类型是表达式的引用。
int func()
{
cout << "调用了func函数" << endl;
return 666;
}
int main()
{
short a = 100;
decltype(a) var;
//var的类型将和a保持一致。
decltype((a)) var2=a;//此时的a用括号括起来了,那么var2就是short&类型
decltype((func)) val3 = func;
//如果func不加(),那val3就是函数类型
//但加了()就是函数类型的引用
val3();//函数类型引用也可以调用函数。
}
- 4)如果上面的条件都不满足,则var的类型与表达式的类型相同。
如果需要多次使用decltype,可以结合typedef和using,给数据类型起别名。
那么我们现在用decltype关键字去解决最初的问题
template<typename T1,typename T2>
void func(T1 a, T2 b)
{
//其他代码
decltype(a+b) tmp=a+b;
cout<<"tmp="<<tmp<<endl;
//其他代码
}
int main()
{
func(3.14,600);
}