当使用者在一个复杂系统上工作并且忽略其基础细节,并对于该系统的表现很满意,那么我们可以说这个系统的设计十分优良。按照这么说,c++中的模板类型推断是很成功的。很多开发者通过向模板函数传递参数得到了完全满意的结果,甚至他们中的很多人只是模糊的知道函数中的类型是如何被推断出来的。
如果你是那些人中的一份子,那我有一个好消息和一个坏消息。好消息是模板类型推断是现代c++中最引人注目的特性之一auto的基础。如果你很满意c++98是如何进行模板类型推断,那么你也会满意c++11中auto是如何进行类型推断的。坏消息是当模板类型推断被用于auto上下文中时,有时候看起来并不像在模板中运用时那么直接。因为这个原因,真正明白auto所依赖的模板类型推断是很重要的。这个Item包含了你需要知道的东西。
如果你可以阅读一些伪码,我们考虑一下下面这个函数:
template<typename T>
void f(ParamType param);
函数调用看起来像这样:
f(expr);
在编译过程中,编译器用expr去推断两种类型:一个是T,另外一个是ParamType。这两种类型往往是不相同的,因为ParamType经常包括修饰符,例如const或者引用限定符。如果模板声明如下:
template<typename T>
void f(const T& param); // ParamType 是 const T&
我们有这样的调用,
int x = 0;
f(x);
T被推断为int,但是ParamType被推断为const int&。
T被推断的类型和传入函数的参数的类型是相同的这一事实很容易理解,也就是说,T就是expr的类型。上例中x是int,T被推断为int。但也不是总是如此。T被推断的类型不仅仅和expr的类型有关,也和ParamType相关。这里有三种情况:
1.ParamType是指针或者引用类型,但不是universal引用(这里不好翻译,因此后文将一直使用universal)(universal引用在Item24中被介绍。在这里你需要知道的就是universal引用的存在以及它与左值引用和右值引用都是不同的)。
2.ParamType是universal引用。
3.ParamType既不是指针也不是引用。
因此我们有三种情形需要检查。每一种都基于我们下面这种一般性的格式以及调用方法:
template<typename T>
void f(ParamType param);
f(expr); //通过expr推断T和ParamType
情形1:ParamType是引用或者指针类型,但不是universal引用。
最简单的情况是当ParamType是引用类型或是指针类型,但不是universal引用。在这种情况中,类型推断像下面这样工作:
1.如果expr的类型是引用,则忽略引用符号。
2.然后用expr的类型和ParamType的情况决定T的类型。
例如,如果这是我们的模板函数:
template<typename T>
void f(T& param); //param 是引用。
我们有如下的变量声明,
int x = 27;
const int cx = x;
const int& rx = x;
如下各种函数调用中,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属性变成了T的类型推断的一部分。
在第三个例子中,注意到尽管rx的类型是引用,但T被推断为非引用。这是因为rx的引用属性在类型推断过程中被忽略。
如果我们将f的参数类型从T&改变为const T&,情况会有少许改变,但是并不会以一种令人惊奇的方法。cx和rx的cosnt属性依旧会被保留,但是因为我们现在假设param是常量引用,所以没有必要再将const作为T的类型推断的一部分:
template<typename T>
void f(const T& param);
int x = 27; //与之前一样
const int cx = x; //与之前一样
const int& rx = x; //与之前一样
f(x); //T是int,param的类型是const int&。
f(cx); //T是int,param的类型是const int&。
f(rx); //T是int,param的类型是const int&。
像之前一样,rx的引用属性在类型推断时被忽略。
如果param是指针(或者常量指针)而不是引用,从本质上来说没什么区别。
template<typename T>
void f(T* param);
int x = 27;
cosnt int *px = &x;
f(&x); //T是int,param的类型是int*。
f(px); //T是const int,param的类型是const int*。
到目前为止,你也许发现自己在打哈切或者快睡着了,因为c++的类型推断规则对于引用或者指针参数是如此的自然,在上述情况中理解它们确实十分无趣。一切看起来都那么明显!一切都是你想象的那样。
情形2:ParamType是universal引用
当模板中有universal引用参数时,情况看起来不是那么明显。这样的参数声明像右值引用(函数模板中有类型T,则universal引用的声明就是T&&),但是当左值参数被传入时他们的表现是不同的。全部的情况在Item24中有说明,而这里是其简略版本(headline version):
1.如果expr是一个左值,T和ParamType都会被推断为左值引用。这是很特殊的一种情况。首先,在类型推断中T被推断为一个引用仅在这种情况中出现。其次,尽管ParamType被声明时与右值引用的语法相同,但它被推断的类型却是左值引用。
2.如果expr是一个右值,则应用常规规则(情形1)。
例如:
template<typename T>
void f(T&& param);
int x = 27;
const int cx = x;
const int& rx = x;
f(x); //x是左值,所以T是int&,param的类型也是int&
f(cx); //x是左值,所以T是int&,param的类型也是int&
f(rx); //x是左值,所以T是int&,param的类型也是int&
f(27); //27是右值,所以T是int,param的类型是int&&
Item24解释了为什么这个示例会有这样的情况。这里的关键点是universal引用参数的类型推断规则是不同于左值引用或是右值引用参数的。尤其是,当universal引用在使用时,类型推断是区分左值参数和右值参数的。这永远不会在非universal引用中发生。
情形3:ParamType既不是指针也不是引用。
当ParamType既不是指针也不是引用时,我们处理的是值传递的情况:
template<typename T>
void f(T param);
这意味着无论传进来的是什么,param都会是其的拷贝,即一个完全的新的对象昂。param将会是一个新对象的事实产生了T如何被expr推断出来的规则:
1.和以前一样,如果expr的类型是一个引用,忽略引用部分。
2.如果在忽略expr的引用属性后expr是const的,也忽略它。如果是volatile的,也忽略它。(volatile对象是非寻常的,一般仅仅用于部署设备驱动程序上,细节请查看Item40。)
因此:
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
注意到尽管cx和rx是const值,param却不是const的。这是可以说得通的。param是一个独立于cx和rx的对象,它是他们的拷贝。cx和rx不能被改变的事实并不能说明param能否被改变。这就是为什么当推断param类型时,expr的const属性(和volatile属性)被忽略:仅仅因为expr不能被改变不意味着它的拷贝不能被改变。
认识到const(和volatile)仅仅在传值参数中被忽略是很重要的。就像我们已经看到的那样,对于参数是常量引用或者常量指针的情况,expr的const属性在类型推断过程中被保留。但是考虑这种情况,expr是指向常量对象的常量指针,并且expr是通过值传递的:
template<typename T>
void f(T param); //param 仍然是值传递
const char* const ptr = “Fun with pointers”; //ptr 是指向常量对象的常量指针
f(ptr);
在这里,右侧的const指ptr不能够被指向一个不同的位置,也不能被设置为null。(左侧的const指ptr指向的对象是const的,它不能被修改。)当ptr被传入f时,指针被拷贝给param。这种情况下,指针本身(ptr)是按值传递的。按照按值传递的参数的类型推断规则,ptr的const属性是被忽略的,param的类型推断会是const char*,即指向一个常量对象的可修改的指针。类型推断时ptr指向的对象的const属性被保留下来,但是当拷贝它以创建一个新指针param时,ptr本身的const属性却被忽略。
数组作为函数参数
主流的模板类型推断涵盖了相当多的情况,但是有一个值得注意的情况需要知道。就是数组类型和指针类型是不同的,尽管他们有时候是可以相互转换的。有这种错误的观念的一个主要原因是,在很多上下文中,数组可以转化为指向其第一个元素的指针。这种落后的转换允许下面的代码通过编译:
cosnt char name[] = “J. P. Briggs”; //name的类型是const char[13]
const char* ptrToName = name; //array decays to pointer
在这里,const char* 指针ptrToName 被name初始化,name的类型是const char[13]。这两种类型并不相同,但是由于数组-指针这种落后的转换规则,代码是可以通过编译的。
但是如果一个数组被传入按值传递的模板中会怎么样?会发生什么呢?
template<typename T>
void f(T param); //按值传递的模板
f(name); //T和param的类型会被推断为什么类型?
我们首先看这种函数参数是数组且没有其他东西的情况(we begin with the observation that there is no such thing as a function parameter that’s an array.)。这是合法的语法。
void myFunc(int param[]);
但是函数声明会被当作指针声明来对待,这意味着myFunc会被同等对待为下面这样的声明:
void myFunc(int* param);
这种数组和指针参数相等的观念继承自c语言,也是c++的基础,并且培养了数组和指针类型是一样的这种错觉。
因为数组参数声明被认为就像指针参数,按值传入模板函数的数组的类型被推断为指针类型。这意味着对f的调用,T的类型被推断为const char*。
f(name); //name是array,但是T被推断为const char*
但是现在有一种“曲线救国”的策略。尽管函数不能声明真正的数组类型,但是他能够声明参数是数组的引用!所以如果我们修改模板函数f,把他的参数变为引用,
template<typename T>
void f(T& param); //传引用参数的模板
我们传递一个数组进去,
f(name); //传递参数进入f
对T的类型推断是真正的数组类型!这个类型包括了数组的size,及在这个例子中,T被推断为const char[13],f的param的类型是const char (&)[13]。
有趣的是,能声明数组引用的能力使得我们能创建一个能推断出数组元素个数的模板函数。
template<typename T,std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept
{
return N;
}
就像Item15解释的一样,将这个函数声明为constexpr使得它的返回值在编译期间即可使用。这让用一个具有相同元素个数的数组(这个数组的大小在大括号初始化时即可计算出来)声明一个数组变得可能:
int keyVals[] = {1,3,7,9,11,22,35}; //keyVals有7个元素
int mappedVals[arraySize(keyVals)]; //mappedVals也是
当然,作为一个现代c++开发者,你更喜欢用std::array这种内置数组类型:
std::array<int,arraySize(keyVals)> mappedVals; //mappedVals的size是7
关于将arraySzie被声明为noexcept,这会帮助编译器产生更好的代码。细节方面请看Item14。
函数作为函数参数
数组并不是c++中唯一的能转化为指针的类型。函数类型也可以转化为函数指针,我们讨论的任何关于数组的类型推断可应用于函数的类型推断中并且他们会转化为函数指针。结果如下:
void someFunc(int,double); //someFunc是一个函数,类型是void(int,double)
template<typename T>
void f1(T param); //在f1中,param按值传递
template<typename T>
void f2(T& param); //在f2中,param按引用传递
f1(someFunc); //param被推断为函数指针,类型是void(*)(int,double)
f2(someFunc); //param被推断为函数引用,类型是void(&)(int,double)
在实际情况中这很少有什么不同,但如果你打算了解数组-指针的转换,你也许同样需要知道函数-指针的转换。
现在你懂了:模板类型推断中auto相关的规则。我开始的时候评论说这些是很直观的东西,在大多数情况下他们的确是。需要特殊对待的就是当对universal引用进行类型推断时左值传入的情况,并且,落后的指针转换规则(对于数组和函数)也有一些混乱。有时候你只是想询问你的编译器,“告诉我你推断的类型是什么?”当那发生时,去看Item4,Item4就是教你通过编译器做到这一点。
Things to Remember
1.模板类型推断过程中,引用参数会被看作非引用,即他们的引用属性被忽略。
2.当对universal引用形参类型推断时,左值参数比较特别。
3.当对按值传递的参数类型推断时,const和volatile属性会被忽略。
4.在模板类型推断时,数组和函数实参转化为指针,除非他们被用来初始化引用。