1.什么是函数模板?
读书的时候我们肯定背过很多作文模板吧,英语也好,语文也罢,背模板都是为了下次能直接套用,节约思考时间。函数模板就和这个作文模板类似,先把模板记录下来,后面在应对不同类型的变量时能做类似的处理
函数模板是通用的函数描述,也就是说它们使用泛型来定义函数,其中的泛型可以用具体的类型来替换。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。
为了声明函数模板,我们引入了template,typename这些关键字,不过typename可以替换为class(这两个关键字是等价的)
写法有两种
template <typename 类型参数1, typename类型参数2, ...>//必须用尖括号
返回值类型 模板名(形参表)
{
函数体
}
template <class 类型参数1, class 类型参数2, ...>//必须用尖括号
返回值类型 模板名(形参表)
{
函数体
}
先看一个例子
template<typename T>//指出建立一个模板,并将类型定义为T
T A(T a,T b)
{
T c=a+b;
return c;
}
这个例子中我们创建了一个A函数模板
不过值得注意的是:模板并不创建任何函数,而只是告诉编译器如何定义函数
第一行代码指出建立一个模板,并将类型定义为T,关键字template和typename是必不可少的,不过typename可以替换为class(这两个关键字是等价的)
template<class T>//这也可以
T A(T a,T b)
{
T c=a+b;
return c;
}
剩下的代码则给A函数下定义
2.怎么调用函数模板呢?
#include<iostream>
using namespace std;
template<class T>
T A(T a,T b)
{
T c=a+b;
return c;
}
int main()
{
int w=9;
int e=1;
int d=A(w,e);//调用模板函数A
cout<<d<<endl;
}
结果是10
工作原理是:编译器将检查所使用的参数类型,并生成相应的函数定义,然后再根据这个生成的定义去调用
上面这个例子中A函数接受两个int参数,因此编译器先生成该函数的int版本,也就是说用int替换所有的T,生成下面这样的定义
int A(int a,int b)
{
int c=a+b;
return c;
}
然后才能被调用,下面这个例子中A函数接受两个double参数,因此编译器先生成该函数的double版本,也就是说用double替换所有的T
我们再看看一个例子,同理
double f=1.0;
double g=9.0;
double h=A(f,g);
cout<<h<<endl;
惊奇的发现打印出来的是10,一个整数!!!难道函数模板没生成double类型的定义吗?实则不然,主要是因为cout的特性:浮点类型被显示为6位,末尾的0不显示,实际上h的值是小数的,并不是函数模板出现了问题
3.函数模板的局限性
上面的A函数中我们将a和b相加,那如果a,b都是数组呢?那么该模板就不适用了!!!!
有两种解决办法
第一种是重载运算符+,这个我们这里不做讨论,感兴趣的可以去了解了解
第二种办法就是,为特定类型提供具体化模板定义,这也叫做显式具体化
4.显式具体化
我们先了解一下什么叫具体化?
模板以泛型的方式描述函数或者类,而具体化使用具体的类型生成函数或类的声明
为函数模板提供一个具体化函数定义,这就叫显式具体化
那怎么做呢?
首先使用显式具体化前,必须有一个与其对应的模板函数
然后我们再来定义显式具体化,显式具体化以template<>打头,并通过名称来指出类型
显示具体化的定义格式有两种
template <> 函数返回类型 函数模板名 <实际参数列表> (函数参数列表)
{
函数体;
}
另一种是
template <> 函数返回类型 函数模板名 (函数参数列表)
{
函数体;
}
这么讲我们可能有点懵,接下来来看个例子
//使用显式具体化前必须要有一个对应的函数模板
template<class T>
T A(T a,T b)
{
T c=a-b;
return c;
}
//定义显式具体化
template <> int* A <int*>(int*a,int*b)
{
int c[2];
c[0]=a[0]-b[0];
return c;
}
我们还可以这样子写
template <> int* A(int*a,int*b)
{
int c[2];
c[0]=a[0]-b[0];
return c;
}
这样子A函数也能用于int指针类型了
值得注意的是具体化函数定义时,函数头必须和它对应的模板函数的返回类型,参数列表一一对应,否则就会出现找不到函数模板原型这种错误。就像上面对应的模板函数是T A(T a,T b),我们提供的具体化函数就不能是D A(T a,T b),这么讲可能有点懵,我们直接上例子
template<class T>
T A(T a, T b)
{
T c = a - b;
return c;
}
//这里我们把具体化函数的返回类型改为void
template<>void A<int*>(int* a, int* b)//这是错误的
{
int c = a[0] - b[0];
cout << c << endl;
}
定义显式具体化,这里我们把具体化函数的返回类型改为void,也就是D A(T a,T b)型了,编译时就会发现这个具体化函数没有对应的函数模板原型,因为它不是T A(T a,T b)型。
5.对于不同版本的同名函数原型,编译器如何选择?
然后对于给定的函数名,可以有非模板函数,模板函数和显式具体化函数以及它们的重载版本
如果有多个原型,编译器怎么选择呢?我们先看个例子
#include<iostream>
using namespace std;
template<class T>
T A(T a, T b)
{
T c = a - b;
cout << "普通模板函数" <<c<< endl;
return c;
}
int main()
{
int a = 9;
int b = 1;
int c = A(a, b);
}
运行结果是普通模板函数8
我们再加上显式具体化函数,在调试一下
#include<iostream>
using namespace std;
//使用显式具体化前必须要有一个对应的函数模板
template<class T>
T A(T a, T b)
{
T c = a - b;
cout << "普通模板函数" <<c<< endl;
return c;
}
//定义显式具体化
template<>int A<int>(int a, int b)
{
int c = a - b;
cout << "显式具体化" << c << endl;
return c;
}
int main()
{
int a = 9;
int b = 1;
int c = A(a, b);
}
运行结果是显式具体化函数8
我们再加上非模板函数试试看
#include<iostream>
using namespace std;
//使用显式具体化前必须要有一个对应的函数模板
template<class T>
T A(T a, T b)
{
T c = a - b;
cout << "普通模板函数" <<c<< endl;
return c;
}
//定义显式具体化
template<>int A<int>(int a, int b)
{
int c = a - b;
cout << "显式具体化" << c << endl;
return c;
}
int A(int a, int b)
{
int c = a - b;
cout << "普通函数" << c << endl;
return c;
}
int main()
{
int a = 9;
int b = 1;
int c = A(a, b);
}
运行结果是普通函数8
结果表明:如果有多个原型,编译器在选择原型时,非模板版本优先于显式具体化版本,显式具体化版本优先于普通模板生成的版本
6.具体化和实例化
具体化是将抽象的概念或概述转化为具体的实际情况或具体的事物。在具体化的过程中,将抽象的概念细化、详细化,使之更加具体、明确。例如,将"动物"这个抽象概念具体化为"狗"、"猫"、"鸟"等具体动物的概念。
隐式实例化,显式实例化,显式具体化都是具体化,它们的相同之处为它们表示的都是使用具体类型的函数定义,而不是使用通用描述
实例化是将抽象的类或模板生成具体的对象,即根据类或模板创建实际的实例。在实例化的过程中,根据类或模板定义的属性和方法,创建出具体的对象,并赋予其属性和行为。例如,根据"狗"这个类创建出具体的"柯基"、"哈士奇"等狗的实例对象。
具体化是从概念到实际情况的转化,而实例化是从类或模板到具体对象的生成过程。
总结:实例化是指编译器在使用模板时根据传递的参数类型生成特定类型的代码,而具体化是指为模板提供特定数据类型的实现,以覆盖通用模板的行为。 实例化和具体化都是模板在编译时根据实际情况生成特定代码的过程,以实现代码的重用和灵活性。
6.1. 隐式实例化:让编译器根据实参推演模板参数的实际类型
函数模板的隐式实例化是在调用函数模板时,编译器根据具体的参数类型自动推断并实例化出对应的函数。
在C++中,函数模板是用来定义通用的函数,能够适应不同类型的参数。当使用函数模板时,需要根据实际情况提供具体的参数类型,用于实例化出对应类型的函数。
例如,下面是一个简单的函数模板示例:
template<typename T>
T add(T a, T b) {
return a + b;
}
当调用add
函数模板时,编译器会根据传入的参数类型自动推断并实例化出对应类型的函数。例如:
int result = add(1, 2); // 隐式实例化为 add<int>(1, 2)
/*系统自动生成int版本的模板函数
int add (int a,int b)
{
return a+b;
}*/
float result = add(1.5f, 2.5f); // 隐式实例化为 add<float>(1.5f, 2.5f)
/*系统自动生成float版本的模板函数
float add(float a,float b)
{
return a+b:
}*/
在上述示例中,编译器会根据调用时传入的参数类型,自动实例化出add<int>
和add<float>
两个函数的定义,这就是隐式实例化。这是系统自动完成的。
特点就是用的时候再生成该版本的函数定义
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供 调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然 后产生一份专门处理double类型的代码,对于字符类型也是如此。
模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;
Add(a1, a2);
Add(d1, d2);
/*
该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,
编译器无法确定此处到底该将T确定为int 或者 double类型而报错
注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅
Add(a1, d1);
*/
// 此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化
Add(a, (int)d);
}
隐式实例化的好处是能够根据具体的参数类型生成对应的函数代码,提高代码的复用性和灵活性。同时,编译器会进行类型检查,确保传入的参数类型满足模板的要求。
6.2.显式实例化
显式实例化和隐式实例化其实差不多
显式实例化就是将上面隐式实例化那个生成某版本的函数定义从系统自动生成改为我们手动去生成,并且可以改变它生成的位置,做到先准备好某版本的模板函数,然后再调用
它只需我们去声明一下就好了,它的声明格式是
template 函数返回类型 模板函数名<实际参数列表>(参数列表);
我们看个例子
int main(void)
{
int a = 10;
double b = 20.0;
// 显式实例化
Add<int>(a, b);
return 0;
}
如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
这个看起来和显式具体化差不多,只不过呢
显式实例化和显式具体化的的区别
1.它们的声明形式不一样
显式实例化是以template 开头,显式具体化是以template<>开头
2.显式实例化和显式具体化的函数名后的<>都可以省略
3.它们的作用不同
注:试图在同一个文件(或转换单元)中使用同一种类型的显式实例化和显示具体化将出错
函数模板的显式实例化是指在编译时强制实例化函数模板,以便在编译期间生成特定类型的函数定义。
以下是一个函数模板的显式实例化实例:
#include<iostream>
using namespace std;
template <typename T>
T A(T a, T b) {
return a > b ? a : b;
}
// 显式实例化int类型的max函数模板
template int A<int>(int a, int b);
int main() {
int a = 5, b = 10;
double c = 3.14, d = 2.718;
// 调用显式实例化的max函数模板
int e = A(a, b);
cout << e << endl;
return 0;
}
在上面的示例中,我们首先定义了一个通用的 max
函数模板,用于比较两个值并返回较大的那个值。然后,我们使用 template
关键字和 <>尖括号来显式实例化 max
函数模板,指定实例化的模板参数类型为 int
。这样,在编译期间会生成一个特定类型的函数定义,以便在程序中调用。
显式实例化可以用于对函数模板进行特定类型的实例化,以避免模板参数的类型推导问题或者优化编译时间。显式实例化的函数模板在编译时会生成特定类型的函数定义,可以提高代码执行效率。
注意,显式实例化只能在函数模板定义所在的文件中进行,且需要提供所需的模板参数类型。
6.3.部分具体化
函数模板的部分具体化是指在函数模板中只对部分模板参数进行具体化,而将其他模板参数保持为泛型。
使用方法和显式具体化类似,不过调用方式略有不同
以下是一个函数模板的部分具体化实例:
template <typename T, typename U>
void print(T t, U u) {
cout << t << " " << u << endl;
}
template<typename T>
void print<int, T>(int t, T u) {
cout << "int " << t << " " << u << endl;
}
int main() {
int a = 5;
double b = 3.14;
print(a, b); // 调用模板函数 void print(T t, U u)
print<int, double>(a, b); // 调用模板函数 void print<int, T>(int t, T u)
return 0;
}
在上面的示例中,我们首先定义了一个通用的 print
函数模板,用于输出两个参数的值。然后,我们使用部分具体化的语法,在模板参数列表中指定了第一个模板参数为 int
,而第二个模板参数保留为泛型 T
。这样,在编译期间会生成一个特定模板参数为 int
的版本的函数定义。
在 main
函数中,我们分别调用了模板函数 print
,第一个调用会调用通用的模板函数,第二个调用会调用部分具体化的模板函数。
部分具体化可以用于针对特定组合的模板参数类型提供不同的实现,可以更灵活地处理不同的情况。
注意,部分具体化只能对函数模板进行,不能对类模板进行部分具体化。
7.关键字decltype
我们在使用函数模板时可能会遇到下面这种情况
template<class T1,class T2>
void A(T1 a,T2,b)
{
?type? c=a+b;//c的类型不确定啊
....
}
这个时候怎么办呢?
C++11引入了关键字decltype,用来推断类型
用法·如下
decltype(expression) var;
expression可以是下面这几种情况
1.没有用括号括起的标识符,var的类型和该标识符的类型相同,包括const等限定符
我们可以举个例子
int x;
decltype(x) y;//将y定义为和x是同一类型的变量
const int y=22;
decltype(y) c=99;//c为const int类型
2.如果expression是一个函数调用,则var的类型与函数返回类型一样
这个过程不会调用函数哦!!!
int A()
{
return 33;
}
int main()
{
decltype(A()) c;//c的类型是A()的返回类型int
}
3,如果expression是一个左值,则var的类型是指向其类型的引用,前提是必须是要用括号括起的标识符
int x=99;
decltype((x)) y=x;
//y的类型就是int&
4.如果和上面三种情况都不符合的话,var的类型和expression的类型相同
decltype(3+9) y;
//3+9为int型,所以y为int型
学会decltype,我们就可以解决上面那个问题了
template<class T1,class T2>
void A(T1 a,T2,b)
{
decltype(a+b) c=a+b;//c的类型就是a+b的类型了
....
}
我们还可能遇到下面这种问题
template<class T1,class T2>
?type? A(T1 a,T2 b)//不知道返回类型是什么
{
return a+b;
}
为了解决这个问题,C++引入了一种新语法
double A();
auto A()->double;
//这两种写法是等价的
这样子我们也可以解决这个问题了
template<class T1,class T2>
auto A(T1 a,T2 b)->decltype(a+b)
{
return a+b;
}