现代C++之理解模板类型推断(template type deduction)

理解模板类型推断(template type deduction)

咱们每每不能理解一个复杂的系统是如何运做的,可是却知道这个系统可以作什么。C++的模板类型推断即是如此,把参数传递到模板函数每每能让程序员获得满意的结果,可是却不可以比较清晰的描述其中的推断过程。模板类型推断是现代C++中被普遍使用的关键字auto的基础。当在auto上下文中使用模板类型推断的时候,它不会像应用在模板中那么直观,因此理解模板类型推断是如何在auto中运做的就很重要了。html

下面将详细讨论。看下面的伪代码:程序员

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

经过下面的代码调用:express

f(expr); //call  f with some expression

在编译过程当中编译器会使用expr推断两种类型:一个T的类型,一个是ParamType。而这两种类型每每是不同的,由于ParamType一般会包含修饰符,好比const或者引用。若是一个模板被声明为下面这个样子:数组

template<typename T>
void f(const T& param);//ParamType is const T&

经过以下代码调用:app

int x = 0;
f(x); //call f with an int

T会被推断成int,可是 ParamType会被推断成const int&。函数

咱们很天然的会认为T的推断类型和传递到函数的参数类型是相同的,上面的例子就是这样的,参数x的类型为int,T也被推断成了int类型。可是每每状况不是这样子的。对T的类型推断不只仅依赖参数expr的类型,也依赖ParamType的形式。指针

有三种状况:code

  • ParamType是指针或者引用类型,但不是universal reference(这个类型在之后的篇章中会讲到,如今只须要明白,这种类型不一样于左值引用和右值引用便可。)
  • ParamType是universal reference。
  • ParamType即非指针也非引用。

下面将分别进行举例,每一个例子都从下面的模板声明和函数调用伪代码演变而来:htm

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

ParamType是指针或者引用类型

这种状况下的类型推断会是下面这个样子:对象

  • 若是expr的类型是引用,忽略引用部分。
  • 而后将expr的类型同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的引用

对param和T的推断以下:

f(x); //T被推断为int,param的类型被推断为 int &
f(cx);//T被推断为const int,param的类型被推断为const int &
f(rx);//T被推断为const int(这里的引用会忽略),param的类型被推断为const int &

第二个和第三个函数调用中,cx和rx传递的是const值,所以T被推断成const int,产生的参数类型就是const int &,当你向一个引用参数传递一个const对象的时候,你不会但愿这个值被修改,所以参数应该会被推断成为指向const的引用。模板类型推断也是这么作的,在推断类型T的时候const会变为类型的一部分。

第三个例子中,rx的类型是引用类型,T却被推断为非引用类型。由于类型推断过程当中rx的引用类型会被忽略。

上面的例子只是说明了左值引用参数,对于右值引用参数一样试用

若是咱们将函数f的参数类型改为cont T&,实参cx和rx的const属性确定不会变,可是如今咱们将参数声明成为指向const的引用了,所以没有必要将const推断成为T的一部分:

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

声明的变量不变:

int x = 27; //不变
const int cx = x;//不变
const int& rx = x;//不变

对param和T的推断以下:

f(x); //T被推断为int,param的类型被推断为const int &
f(cx);//T被推断为int,param的类型被推断为const int &
f(rx);//T被推断为int(引用一样被忽略) ,param的类型被推断为const int &

若是param是指针或者指向const的指针,本质上同引用的推断过程是相同的。

指针和引用做为模板参数在推断过程当中的结果是显而易见的,下面的例子就隐晦一些了。

ParamType是一个Universal Reference

这种类型的参数在声明时形式上同右值引用相似(若是一个函数模板的类型参数为T,将其声明为Universal Reference写成TT&&),可是传递进来的实参若是为左值,结果同右值引用就不太同样了(之后会讲到)。

Universal Reference的模板类型推断将会是下面这个样子:

  • 若是expr是一个左值,T和ParamType都会被推断成左值引用。有点难以想象,首先,这是模板类型推断中惟一将T推断为引用的状况;其次,虽然ParamType的声明使用右值引用语法,但它最终却被推断成左值引用。
  • 若是expr是一个右值,参考上一节(ParamType是指针或者引用类型)。

举个例子:

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

int x = 27; //不变
const int cx = x;//不变
const int& rx = x;//不变

对param和T的推断以下:

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&&

这里的关键点是,模板参数为Universal Reference类型的时候,对于左值和右值的推断状况是不同的。这种状况在模板参数为非Universal Reference类型的时候是不会发生的。

ParamType既不是指针也不是引用

这种状况也就是所谓的按值传递:

template <typename T>
void f(T param);//按值传递

传递到函数f中的实参值会是原来对象的一份拷贝。这决定了如何从expr中推断T:

  • 同状况一相似,若是expr的类型是引用,忽略引用部分。
  • 若是expr是const的,一样将其忽略。若是是volatile的,一样忽略。

看例子:

int x = 27; //不变
const int cx = x;//不变
const int& rx = x;//不变

对param和T的推断以下:

f(x); // T为int ParamType为 int
f(cx);//同上
f(rx);//同上

能够看到即便cx和rx为const,param也不是const的。由于param只是cx和rx的一份拷贝,因此不论param的类型如何都不会对原值形成影响。不能修改expr并不意味着不能修改expr的拷贝。

注意只有param是by-value的时候,const或者volatile才会被忽略。咱们在前面的例子中说明了,若是参数类型为指向const的引用或者指针,类型推断过程当中expr的const属性会被保留。可是看一下下面的状况,若是expr为指向const对象的const指针,而param的类型为by-value,结果会是什么样子的呢:

template <typename T>
void f(T param);//按值传递

const char * const ptr = "Fun with pointers";
f(ptr);

咱们先回忆一下const指针,星号左边的const(离指针最近)表示指针是const的,不能修改指针的指向,星号右边的const表示指针指向的字符串是const的,不能修改字符串的内容。当ptr传递给f的时候,指针自己是按值传递的。由于在by-value参数的类型推断中const属性会被忽略,所以指针的const也就是星号右边的const会被忽略,最后推断出来的参数类型为const char * ptr,也就是能够修改指针指向,不能修改指针所指内容。

数组参数

上面的三种状况涵盖了模板类型推断的大部分状况,可是有另一种状况不得不说,就是数组。虽然数组和指针有时候看上去是能够互换的,形成这种幻觉的一个主要缘由是在许多状况下,数组能够退化为指向第一个数组元素的指针,正是这种退化下面的代码才能编译经过:

const char name[]="HarlanC";//name的类型为const char[8]
const char*ptrToName = name;//数组退化成指针

虽然指针和数组的类型不一样,但因为数组退化为指针的规则,上边的代码可以编译经过。

若是将数组传递给带有by-value参数的模板,会发生什么呢?

template <typename T>
void f(T param);//按值传递
f(name);

将数组做为函数参数的语法是合法的。

void myFunc(int param[]);

可是这里的数组参数会被当作指针参数来处理,也就是说下面的声明和上面的声明是等价的:

void myFunc(int* param); // same function as above

由于数组参数会被当作指针参数来处理,因此将一个数组传递给按值传递的模板函数会被推断为一个指针类型。当调用模板函数f的时候,类型参数T会被推断成const char*:

f(name); // name is array, but T deduced as const char*

虽然函数不能声明一个真正的数组参数(即便这么声明也会被当作指针来处理),可是可以将参数声明为指向数组的引用。咱们将模板函数作以下修改:

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

传递一个数组实参:

f(name);

这时候会将T推断成一个真正的数组类型。这个类型同时包含了数组的大小,在上面的例子中,T会被推断成const char [8],而f的参数类型为const char (&)[8]。

使用这种声明有一个妙用。咱们能够建立一个模板来推断出数组中包含的元素数量

//在编译期返回数组大小 ,
//注意下面的函数参数是没有名字的
//由于咱们只关心数组的元素数量
template<typename T, std::size_t N> 
constexpr std::size_t arraySize(T (&)[N]) noexcept 
{ 
    return N; 
}

将函数返回值声明成constexpr类型的意味着这个值在编译期就可以获得。这样咱们能够在编译期获取一个数组的大小,而后声明另一个相同大小的数组:

int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 };
int mappedVals[arraySize(keyVals)];

使用std::array更可以体现你是一个现代C++程序员:

std::array<int, arraySize(keyVals)> mappedVals;

函数参数

数组不是可以退化成指针的惟一类型。函数类型也可以退化为指针,咱们所讨论的关于数组的类型推断过程一样适用于函数:

void someFunc(int, double); // someFunc是一个函数,类型为void(int, double)

template<typename T>
void f1(T param); //passed by value
template<typename T>
void f2(T& param); // passed by ref
f1(someFunc); // param 被推断为 ptr-to-func void (*)(int, double)
f2(someFunc); // param 被推断为ref-to-func void (&)(int, double)

要点总结

  • 模板类型推断会把引用当作非引用来处理,也就是说会把参数的引用属性忽略掉。
  • 当模板参数类型为universal reference 时,进行类型推断会对左值入参作特殊处理。
  • 当模板类型参数为by-value时,const或者volatile会被当作非const或者非volatile处理。
  • 当模板类型参数为by-value时,入参为函数或者数组时会退化为指针。
相关文章
相关标签/搜索