马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
假设我们需要一个求和函数,我们可能会写成这样:
double Add(int a, int b){
return a + b;
}
当用户传递给该函数两个整型实参时,函数正常运行。
但是当用户传递两个double类型,比如Add(2.5, 3.6),或者一个int一个double,比如Add(2, 3.6),或者更多其它类型的组合的时候,编译器便找不到相匹配的函数。
于是,我们不得不使用重载,将可能出现的情况一一列举:
double Add(int a, int b)
{
return a + b;
}
double Add(double a, double b)
{
return a + b;
}
double Add(int a, double b)
{
return a + b;
}
double Add(double a, int b)
{
return a + b;
}
...
...
我们发现,上面那么多的重载函数,除了参数的类型不同以外,其它的所有部分都是一样的。
那么有没有一种方法,可以代替我们进行这些廉价的体力劳动呢?
答案就是:模板
模板的一般形式如下
template<typename T, typename U> // 定义了两种类型 T U
double Add(T a, U b) // 使用类型 T U 分别定义了形参
{
return a + b;
}
其中template<>里面typename X的个数是可选的,可以是一个,也可以是多个。
模板是怎么实现自定义类型的呢?
具体的流程可以这样理解:
当我们在IDE写下这样的代码:
#include <iostream>
template<typename T, typename U>
double Add(T a, U b)
{
return a + b;
}
int main(int argc, char *argv[])
{
int a = 1;
int b = 2;
double c = 3.6;
Add(a, b);
Add(b,c);
}
当我们点下编译按钮的时候,编译器所做的事情可以理解为一下两步。
第一遍编译器发现有一个模板函数Add(),然后编译器在整个代码里面找,在哪里调用了这个求和函数。最终编译器在mian找到了两处调用,编译器一看,这两处调用,一次是传了两个int,一次是传了一个int和一个double。
第一遍分析完了,编译器要做的第二步就是,将源码中的模板删掉,然后用具体的参数类型替换掉模板中的T和U类型,也就是说,上面的代码,经过编译器处理之后,就变成了这样:
#include <iostream>
double Add(int a, int b)
{
return a + b;
}
double Add(int a, double b)
{
return a + b;
}
int main(int argc, char *argv[])
{
int a = 1;
int b = 2;
double c = 3.6;
Add(a, b);
Add(b,c);
}
最后,编译器将重写之后的代码再进行编译,所有的函数调用都可以找到相匹配的实现了。
简单来说,编译器做的事情有两件:第一个是找到模板并找到哪些地方调用了模板,第二个就是根据调用模板的实际类型替换掉模板,生成没有模板的代码。最后就可以再次编译运行了。
模板推导
模板推导就是根据传递给模板的实参类型,推导出应该生成的模板代码。
比如:
template<typename T, typename U> // 定义了两种类型 T U
double Add(T a, U b) // 使用类型 T U 分别定义了形参
{
return a + b;
}
Add(1,2); // T被推导为int类型
模板推导是在编译器期间做的事情,而推导的规则基本上是符合我们预料的情况。但是具体来说,可以分为三大类型: 按值传递的模板推导按引用传递的模板推导万能引用本节主要介绍前面两点。
按值传递
按值传递的模板形式如下:
template<typename T>
ReturnType Function(T param)
{
// 函数实现
}
注意模板参数的形式,必须是T param的形式才是表达的按值推导。
因为按值传递的实质是对实参对象进行拷贝,得到实参的一个副本。所以,按值传递的时候,实参的常量性和引用性都会被忽略。
下面是几个模板推导的实例
#include <iostream>using namespace std;
template<typename T>
void Function(T param)
{
// 函数实现
}
int main(int argc, char *argv[])
{
int a = 1;
const int b = 2;
int &ra = a;
const int &rb = b;
int *pa = &a;
const int *pb = &b;
const int *const cpb = &b;
Function(a); // T 推导为int
Function(b); // T 推导为int,忽略b的常量性
Function(ra); // T 推导为int,忽略引用性
Function(rb); // T 推导为int,忽略常量性和引用性
Function(pa); // T 推导为int*,int*和int是不同的类型,
Function(pb); // T 推导为const int *
// 这里的const并不是pb本身的常量性,而是pb所指对象的常量性,所以得以保留
Function(cpb); // T 推导为const int *,和上一条对比,cpb本身的常量性被忽略了
}
按引用传递
如果你想要模板推导为引用类型,那么你的模板应该是这样的:
template<typename T>
ReturnType Function(T& param)
{
// 函数实现
}
以下是几个实例:
#include <iostream>using namespace std;
template<typename T>
void Function(T& param) // 传递引用的模板
{
// 函数实现
}
int main(int argc, char *argv[])
{
int a = 1;
const int b = 2;
int &ra = a;
const int &rb = b;
int *pa = &a;
const int *pb = &b;
const int *const cpb = &b;
Function(a); // T 推导为int
Function(b); // T 推导为const int
Function(ra); // T 推导为int,忽略ra引用性
Function(rb); // T 推导为const int,忽略rb引用性
Function(pa); // T 推导为int*,
Function(pb); // T 推导为const int *
Function(cpb); // T 推导为const int *const,和上一条对比,cpb本身的常量性被忽略了
}
以上推导的规则就是,如果传递进去的是一个值,那么就生成对该值类型的引用模板,因为引用参数可以接受值类型参数。如果传递进去的是一个引用类型,那么模板将忽略实参的引用性(因为模板里已经明确写了T&,所以实参的引用性可以忽略不看),然后再进行推导。比如,传递进去是rb,rb的类型是const int &,忽略引用性就剩下const int,这就是 T 被推导的类型了。
关于推导数组类型和函数指针类型
对于这两种,不作更多的解释。大家记住就好,模板推导这东西,本身就没啥固定的规则可言,基本的原则就是要符合用户的预期。
数组模板:
#include <iostream>using namespace std;
template<typename T>
void ArrayPattern1(T& param) // 数组模板
{
// ...
}
template<typename T, size_t N>
void ArrayPattern2(T (¶m)[N]) //数组模板,并推导出数组元素个数
{
// ...
}
template<typename T>
void PrintType(T param)
{
}
int main(int argc, char *argv[])
{
int a[10]{ 0 };
ArrayPattern1(a); // T的类型为 int[10] , param的类型为 int(&)[10]
ArrayPattern2(a); // T的类型为 int, param的类型为 int(&)[10]
PrintType(a); // 按值传递,a退化为指针,丢失了数组元素个数的信息
}
函数模板
函数模板既可以按值传递,也可以按引用传递。效果是一样的。
|