用一段伪代码来介绍模板函数类型推导要做的事情是什么
//声明一个模板函数f
template
void f(ParamType param);
//调用该模板函数
f(expr);
如上代码片段,通过调用模板函数,依据实参 expr 来推导出 T 与 ParamType 的类型就是模板函数类型推导要做的事情,具体来讲,两件事:一是推导 T 的类型;二是推导 ParamType 的类型。
通常来讲, T 与 ParamType 的类型往往是不一样的,因为在ParamType中通常会加一些如const或引用饰词等,如下代码片段
int x = 27;
//声明模板函数
template
void f(const T& param);
//调用该模板函数
f(x); // 此时T类型为 int, ParamType类型也为const int&
如上所示,T 与 ParamType 的类型结果是不一致的,但 T 与实参 x 的类型是一致的,那是不是说所有情况下 T 与实参的类型都会保持一致,事实也并非想象的这么简单,T的类型不仅取决于实参 expr,同时也取决于 ParamType 的形式,需要分以下三种情况来讨论:
a. 当 ParamType 为指针或者引用时,但此时的引用不是万能引用;
b. 当 ParamType 为万能引用时;
c. 当 ParamType 既非指针也非引用时;
此时的模板类型推导规则如下:
若实参 expr 是引用或指针类型时,则将 引用或指针 属性先忽略掉;
然后对 expr 的类型与 ParamType 的类型执行模式匹配,来决定 T 的类型;如下代码片段
int x = 27;
const int cx = x;
const int &rx = x;
//声明模板函数
template
void f(T& param); //此时ParamType为引用类型;
//调用模板函数
f(x); //实参x为非引用类型int, 则T为int, ParamType为int&
f(cx); //实参cx为const int, 则T为const int, ParamType为const int&
f(rx); //实参rx为引用类型,则先将引用性忽略,T为const int, ParamType为 const int&
如果将 ParamType 的类型从 T& 改为 const T& 时,T 的类型推导结果就会有一些变化,因为此时已经假设形参是一个const 类型的引用了,所以在T的类型推导结果中如果含有const就没有必要了,如下代码片段;
const int cx = 27;
//声明模板函数
template
void f(const T& param);
//调用模板函数
f(cx); //ParamType含有const饰词,则T的类型为int, ParamType的类型为const int&
若当ParamType为指针类型时,则推导原则和上面一致,如下代码片段
int x = 27;
const int *px = &x;
//声明模板函数
template
void f(T* param);
//调用模板函数
f(&x); //T为int, ParamType为int*;
f(px); //现将指针属性忽略,T为const int, ParamType为const int*;
此时的类型推导规则比较特殊,特殊之处有二,其一,是唯一一种来类型推导时需要区分实参为左值还是右值的情况;其二,是唯一一种将T推导为引用类型的情况,具体规则如下:
当实参为左值时,则 T 与 ParamType 的类型均推导为左值引用类型;
当实参为右值时,则 T 为相应的右值类型,ParamType 为相应类型的右值引用;
如下代码片段所示:
int x = 27;
const int cx = x;
const int &rx = x;
//声明模板函数
template
void f(T&& param);
//调用该模板函数
f(x); // x 为左值,则 T 为 int&, ParamType 也为 int&
f(cx); // cx 为左值,则 T 为const int&, ParamType 也为 const int&
f(rx); // rx 为左值,则 T 为const int&, ParamType 也为 const int&
f(27); // 27 为右值,则 T 为int, ParamType 为 int&&
这也是当ParamType为万能引用时,唯一会区分实参为左值还是右值的情况,其他非万能引用时,从来不会做这样的区分。
当ParamType既非指针也非引用时,也就是所谓的按值传递,此种情况形参本质上是实参的一个拷贝,形参与实参是不同的两个对象,在内存中有着不同的存储空间,这样就促成了此种情况下的类型推导规则:
当实参具有引用属性时,引用性将被忽略;当其具有const常量性时,将被忽略;当其具有volatile属性时,同样将被忽略掉;如下代码片段:
int x = 27;
const int cx = x;
const int& rx = x;
template
void f(T param);
f(x); // T 为 int, ParamType 也为int
f(cx); // T 为 int, ParamType 也为int
f(rx); // T 为 int, ParamType 也为int
因为形参是一个全新的对象,所以当实参具有某种特性时(引用性或者常量性或者volatile性),并不意味着形参也该具有这样的特性。
需要说明的是只有在按值传递时,const及volatile特性才会被忽略,但考虑下面这样的情况:如果ptr是一个指向常对象的常量指针时,而且按值传递时,类型推导结果会怎样呢:
const char* const ptr = "Hello world, hello dream.";
// * 左侧的 const 代表它指向一个常对象,不可更改的对象;
// * 右侧的 const 代表指针的指向不能更改,它本身是一个常指针
template
void f(T param);
f(ptr); // 此时ptr本身的指向不能更改的常量性将被忽略,但指向对象的常量性将会保留
// 所以 param 是个 const char* 类型的一个全新的指针,该指针可以修改指向
有关模板函数类型推导的情况已经基本讨论完了,但是还有一个情况需要值得留意,数组实参。
如果将数组作为实参传递给一个函数,那么该函数的形参应该怎么定义呢,按理来说应该是下面这样的:
int nArr[5] = {0};
void myFunc(int param[]);
以上代码的语法是合法的,但是我们很少将形参定义为数组类型,由于在C语言中数组的声明可以按照指针的声明加以处理,所以上面代码也可以写成这样的形式:
void myFunc(int *param);
这种数组和指针形参的等价性是C语言的遗迹,这种写法更容易让人理所应当的认为数组和指针类型就是一回事儿,容易混淆视听。那如果将数组类型按值传递的方式传递给模板函数,会有怎样的推导情况呢,如下代码片段:
const char name[] = "hello world, hello dream"; //name 类型为 const char[25]
template
void f(T param);
f(name); //此时 T 的类型为 const char *
如上所示,T 的类型被推导成 const char* 指针类型,并非数组类型;有意思的是,如果按引用的方式来传递数组类型,则 T 会被推导成实际的数组类型,如下代码片段:
const char name[] = "hello world, hello dream";
template
void f(T& param); //按引用方式传递形参的模板
f(name); // T 的类型为 const char[25]; ParamType 的类型为 const char(&)[25];
正如上面示例代码,T 会被推导为实际的数组类型,这个类型中会包含数组尺寸。形参类型被推导为数组的引用。
上面讨论的数组实参其实是C++里面的一种退化机制,从数组退化为指针;但并非只有这一种退化,函数的类型也可以退化成函数指针;并且将函数作为实参的模板类型推导与数组实参一模一样;如下代码片段:
void funcExample(int, double); //funcExample是个函数,其类型为 void(int, double);
template
void f1(T param); //param按值传递
template
void f2(T& param); //param按引用传递
f1(funcExample); //此时 param 的类型为函数指针 void(*)(int, double);
f2(funcExample); //此时 param 的类型为函数引用 void(&)(int, double);
以上就是关于模板函数类型推导的所有内容了,除了讨论了ParamType三种形式下的类型推导,还说明了两种特殊情况下的类型推导机制。如果有帮助到您,那就点个赞吧哈哈~