zl程序教程

您现在的位置是:首页 >  其他

当前栏目

Item1——理解模板类型推导(Understand template type deduction)

模板 类型 理解 type Template 推导 Understand
2023-09-14 09:14:58 时间

条款1——理解模板类型推导

本小节主要介绍模板函数的推导类型。在一般情况下,我们可以根据模板调用很快就看出模板的类型。但是在复杂的场景中,我们无法快速看出来,这就需要我们去了解并且属性模板类型的推导规则

template<typename T>
void f(ParamType param);

f(expr); //通过expr 来推导 T 和 param 的类型

T 类型的推导结果,依赖于 e x p r expr expr 的类型 , 还依赖 P a r a m T y p e ParamType ParamType 的形式。这里需要分为三种不同的情况讨论:

  • P a r a m T y p e ParamType ParamType 是引用或指针类型,但不是万能引用
  • P a r a m T y p e ParamType ParamType 是万能引用
  • P a r a m T y p e ParamType ParamType 即不是指针也是引用

Case 1 : P a r a m T y p e ParamType ParamType 是一个指针或者引用,但不是万能引用

这种情况下的推导规则为:

  • e x p r expr expr 是引用类型,则会先将引用部分忽略
  • 接着,再对 e x p r expr expr 的类型和 P a r a m T y p e ParamType ParamType 的类型执行模式匹配 , 来决定 T 的类型

第一个例子:

template<typename T>
void f(T& param);

声明了以下的变量:

int x = 27;              //x 的类型为 int
const int cx  = x;       //cx 的类型为 const int
const int& rx = x;       //rx 的类型为 const int 的引用

在三次调用中,对 p a r a m param param 和 T 的类型推导结果为:

f(x);                   //T 的类型是 int param 的类型是 int&

f(cx);                 //T 的类型是 const int param 的类型是 const int&

f(rx);                 //T 的类型是 const int param 的类型是 const int&

注意:

  • 在第二个,三个调用中,因为rx 和 cx 都被声明为const int。所以T 也被推导为 const int,形参被推导为const int&。向有 T& 类型的模板传入 const对象是安全的,因为该对象的常量属性会成为 T 的类型推导结果的组成部分。
  • 在第三个调用中,即使 rx 有引用类型,T 也没有被推导为引用。因为 rx 的引用性(referednce-ness)会在推导过程中被忽略。

第二个例子:


template<typename T> 
void f(const T& param);      //现在 param 是一个常量引用了


int x = 27;              //x 的类型为 int
const int cx  = x;       //cx 的类型为 const int
const int& rx = x;       //rx 的类型为 const int 的引用

f(x);                   //T 的类型是 int param 的类型是 const int&

f(cx);                 //T 的类型是  int param 的类型是 const int&

f(rx);                 //T 的类型是  int param 的类型是 const int&

第三个例子:

template<typename T> 
void f(T* param);      //现在 param 是一个指针 推到方式也是类似的


int x = 27;              //x 的类型为 int
const int* px = &x;

f(&x);                   //T 的类型是 int param 的类型是 int*

f(px);                 //T 的类型是  cosnt int  param 的类型是 const int*

Case 2 : P a r a m T y p e ParamType ParamType 是一个万能引用

对于有万能引用形参的模板而言,规则就没那么简单了。此类形参的声明方式类似于右值引用(即在函数模板中有类型形参T时,万能引用的声明类型写作 T&&), 更详细的在 Item 24 中展开介绍。

  • 如果 e x p r expr expr 是左值, T 和 P a r a m T y p e ParamType ParamType 都会被推到为左值引用
  • 如果 e x p r expr expr 是右值, 则复合Case 1 中的规则

例子:

template<typename T>
void f(T&& param)  //param 现在是一个 万能引用

int x = 27;
const int cx = x;
const int& rx = x;

f(x);              //x 是一个左值,所以 T 的类型为 int&  , param 的类型也是 int&

f(cx) ;            //cx是一个左值,所以 T 的类型为 const int&  , param 的类型也是 const int&

f(rx);             //rx是一个左值,所以 T 的类型为 const int&  ,  param 的类型也是 const int&

f(27);            //27是一个右值,所以 T 的类型为 int   ,  param 的类型就为 int&&

注意:

  • 万能引用形参的推导规则不同于左值引用 和 右值引用形参。当遇到万能引用的时候,类型推导规则会区分实参是左值还是右值。非万能引用不会做这样的区分。

Case 3 : P a r a m T y p e ParamType ParamType 既不是指针也不是引用

P a r a m T y p e ParamType ParamType 既不是指针也不是引用时,也就是所谓的值传递的时候。因为值传递相当于是拷贝实参的一个副本,无论这个副本发生什么变化,都不会影响到实参。

  • 如果 e x p r expr expr 是引用类型,则忽略其引用部分
  • 如果 e x p r expr expr 是const对象,则忽略。是volatile对象也忽略。

第一个例子:

template<typename T>
void f(T param);             //此时param 是值传递

int x = 27;              
const int cx  = x;       
const int& rx = x;       

f(x);                   //T 和 param 的类型都是 int

f(cx);                 //T 和 param 的类型都是 int

f(rx);                 //T 和 param 的类型都是 int

注意:即使rx 和 cx 都有const 也不会影响到身为副本的param。同样实参若有volatile,也不会影响到副本parameter。

需要说明的是,const 和 volatile 仅仅会在 按值形参的地方被忽略。如果形参是 const 的引用或者指针, e x p r expr expr 的常量属性会在类型推导的过程中加以保留。

第二个例子:

template<typename T>
void f(T param); 

const char* const ptr = "感觉不如原神...画质";    //ptr 是一个指向 const对象 的 const 指针

f(ptr)                  //param 的类型为 const char* 

ptr 这个指针本身会按比特复制给 param,这个指针自己会被按值传递。在类型推导的过程中,ptr指向对象的常量属性得到保留,但自身的常量属性则会在以复制方式创建新指针 param 的过程中被忽略。

数组实参

除了以上三种主流的情况以外,还有一些少数的例子——传递数组的情况。

一般来说数组类型是不同于 指针类型的,尽管有时候它们看起来可以互换。造成这种情况的原因是:有时候数组会退化成指向其首元素的指针,以下代码就是例子:

const char name[] = "Genshin...";  //name 的类型为 const char[13]

const char* ptr = name;           //数组退化成了指针

这时我们考虑 把 数组传递给 值传递的函数模板的情况

template<typename T>
void f(T param);

f(name);             

由于数组形参声明会按照它们好像是指针形参那样处理,按值传递给函数模板的数组类型将被推导成指针类型。

f(name);       //name虽然是一个数组,但是 T 的类型却被推导成 const char *

值得注意的是:尽管函数无法声明真正的数组类型的形参,但是却可以将形参声明为数组的引用!

template<typename T>
void f(T& param);          //此时是按引用传递

f(name);                   //T 被推导为 const char[13]  ,param 的类型为 const char (&) [13]

我们利用声明数组引用的这一特性可以写一个模板,用来推导出数组含有的元素个数:

//以编译期常量的方式返回数组的元素个数
//该数组形参没起名字,在这里只关心其中含有元素的个数
template<typename T, std::size_t N>                  
constexpr std::size_t arraySize(T (&)[N]) noexcept  
{                 
  return N;
}       

通过这个模板函数,我们可以声明一个数组,它的元素个数 和 第一个数组的元素个数相同

int keyVals[] = { 1,2,3,4,5,6,7 }; 
int mappedVals[arraySize(keyVals)];
std::array<int, arraySize(keyVals)> mappedVals;

函数实参

与数组相同,函数也会退化成函数指针。所以两者的推到规则也大致相同。

void someFunc(int, double);  

template<typename T>
void f1(T param);     // f1 按值传递     
f1(someFunc);         //param是函数指针 类型为 void(*)(int,double)   

template<typename T>
void f2(T& param);     // f2 按引用传递  
f2(someFunc);          // param是函数引用 类型为 void(&)(int,double)
                        

总结

  • 在模板类型推导过程中,具有引用类型的实参会被当成非引用类型来处理。即它的引用属性会被忽略
  • 对万能引用形参进行推导时,左值实参进行特殊处理
  • 对按值传递的形参进行推导时,如果其实参类型带有 const 或 volatile 修饰词,则会忽略其属性
  • 在模板类型推导过程中,数组 或 函数类型的实参会退化成对应的指针,除非它们被用来初始化引用