前言
如果开发者在不了解某系统运行机制的前提下仍能高效地使用它,他们往往会给予该系统极高的赞美。由此说来,C++中的模板类型推衍无疑是一项巨大的成功,数以万计的开发者轻松获取了满意的结果,尽管他们不得不为了描述这些经由推衍得到的类型而做出相当大的努力。
如果你就是上述开发者中的一员,迎接你的是一个好消息与一个坏消息。好消息是模板类型推衍技术是auto
技术的基础,如果你对于模板类型推衍感到满意,那么想必你也会认为auto
老少咸宜。坏消息是auto
技术中所使用的类型推衍技术相较于template可能不够直观。为了清楚地了解auto
,我们首先最好认知一下模板类型推衍。
问题实例
考虑以下函数模板及其调用:1
2
3
4template<typename T>
void f(ParamType param);
f(expr);
在编译期间,编译器会使用expr去推衍T、ParamType。这两者往往存在一定的差异,原因在于ParamType可能会带有一定的修饰,例如const或者reference之类。举例而言,如果该函数模板被声明和使用如下:1
2
3
4
5template <typename T>
void f(const T& param);
int x=0;
f(x);
那么T将被推衍为int类型,而ParamType将被推衍为const int&。
认为实参类型类型等价于T的类型是一种十分自然的想法,就像刚才的T为int一样,但很多时候情况并不是这样,事实上,T的类型并不完全取决于expr的类型,也与ParamType的形式有关。以下将给出三组案例:
- ParamType是一个pointer或reference,但其非universal reference。(universal reference将在Item24中给出解释,这里只需要明确其并非左值引用或右值引用)
- ParamTyp是一个universal reference。
- ParamType既不是pointer也不是reference。
Refernece or Pointer,and not a universal reference
这种情况是最简单的情况,在此情况下,类型推衍遵循以下原则:
1. 如果expr的类型是一个refernce,则忽略refernece部分。
2. 根据expr与ParamType的类型完成T类型推衍
举例而言,现有声明与应用如下所示:1
2
3
4
5
6
7
8
9
10
11// function template declartion
template <typename T>
void f(T& param);
//variable declaration
int x =27;
const int cx = x;
const int& rx =x;
f(x);//T is int,param's type is int&
f(cx);//T is const int,param's type is const int&
f(rx);//T is const int,param's type is const int&
需要注意的是第二和第三个函数调用,由于cx与rx代表常量,因此T是一个const int,param的类型是const int&。当你试图传递一个常量给引用参数时,编译器会认为对象仍然需要保持不可变性,因此param会成为一个reference-to-const。我们可以认为,const对象传递给引用类型的形参是安全的,编译器会推衍出T带有const属性。
在第三个函数调用中,rx本身是一个reference,而T被推衍为non-refernce,这是由于rx的reference属性在类型推衍中被忽略所致。
上述的三个实例都是左值引用,但实际上右值引用的类型推衍并没有什么不同,非要说的话就是只有右值实参才能被赋予右值形参。
如果我们将template function的形参由T&改为const T&,事情会发生一点点小小的变化。cx与rx的const属性仍然得到了关注,但由于我们已经认定param是一个reference-to-const,因此T将被推衍为int类型,即:1
2
3
4
5
6
7
8
9
10template <typename T>
void f(const T& param);
int x = 27;
const int cx = x;
const int& rx =x;
f(x);//T is int,param's type is const int&
f(cx);//T is int,param's type is const int&
f(rx);//T is int,param's type is const int&
当然,这里rx的reference属性也被忽略了。
如果param是一个pointer或pointer-to-const,那么类型推衍结果几乎没什么变化:1
2
3
4
5
6
7
8template<typename T>
void f(T* param);
int x = 27;
const int *px = &x;
f(&x);//T is int,param 's type is int*
f(px);//T is const int,param's type is const int *
ParamType is a Universal Reference
在上一案例中,一切推衍都十分自然。但当template以universal reference作为参数时,推衍法则将变得不再显然。某些parameters被声明为右值引用,但当左值传入时,其表现的结果将不再类似于右值引用。关于universal reference的详细介绍将在Item24展开,接下来将给出本案例的推衍法则:
- 如果expr是一个左值,那么T与ParamType都将被推衍为左值引用。这里存在两点不同寻常之处,首先,只有在此种情况下template才会将T推衍为引用类型;其次,尽管ParamType被声明为右值引用,但最终其推衍类型是一个左值引用。
- 如果expr是一个右值,则遵照案例1中的法则执行。
有问题实例如下:1
2
3
4
5
6
7
8
9
10
11template<typename T>
void f(T&& param);
int x= 27;
const int cx = x;
const int& rx =x;
f(x);//x is lvalue,T is int&,param's type is int&
f(cx);//cx is lvalue,T is const int&,param's type is const int&
f(rx);//rx is lvalue,T is const int&,param's type is const int&
f(27);//27 is rvalue,T is int,param's type is int&&
在Item24中将会详细解释此处的类型推衍法则原理。在Universal reference案例中,关键点在于实参是左值还是右值会导致不同的推衍原则被执行,这在non-universal reference中不会出现。
ParamType is Neither a Pointer nor a Reference
这种情况意味着执行值传递——产生了一个对象的copy,这一现象产生了本案例下的类型推衍法则:
- 如果expr为reference,则忽略其reference属性。
- 如果在忽略reference属性后,expr还留有const或volatile属性,则一并忽略。
有实例如下:1
2
3
4
5
6
7
8
9
10template <typename T>
void f(T param);
int x = 27;
const int cx = x;
const int& rx = x;
f(x);//T is int,param's type is int
f(cx);//T is int,param's type is int
f(rx);//T is int,param's type is int
这里需要注意的是,即使cx与rx代表常量,param也并非const。我们应当明确,param是一个与cx或rx无关的对象,它由copy得到,这很好地解释了为什么param不会具备expr所带有的constness与volatileness。不过需要说明的是,constness或volatilness只在pass-by-value中会被忽视,pass-by-pointer或pass-by-reference则会保留其特性。最后,让我们看看以下这种情况:1
2
3
4
5template <typename T>
void f(T param);
const char* const ptr = "Hello,world!";
f(ptr);//T is const char*, param's is const char*
在本实例中,我们需要明确的是param是一个ptr的拷贝,因此底层const属性被丢失(bitwise copy),而顶层const则得到了保留。
Array Arguments
尽管我们经常在实际使用中将数组类型与指针混为一谈,但在类型推衍中它们存在一定的区别。我们首先复习一下数组名与指针的关系:1
2const char name[] = "J.P.Briggs";//name's type is const char[13]
const char* ptrname = name;//array decays to pointer
显然,ptrname经由name得到了初始化,尽管它们的类型并不相同,但由于退化原则(array-to-pointer),上述代码得以被编译。不妨试想,如果一个数组以pass-by-value方式传递给template,将会如何?1
2
3
4template <typename T>
void f(T param);
f(name);
为了解决这个问题,我们先从一个更加基础的问题开始回答:当数组作为一个函数的参数时会发生什么?1
void myFunc(int param[]);
此处的形参尽管被声明为数组,实际上编译器会将其视为一个指针,因此,myFunc声明式等价于1
void myFunc(int* param);
数组名与指针等价的规则起始于C语言,这一设定也被C++继承,这直接导致了开发者产生了数组等价于指针的错误观念。基于这一规则,我们在类型推衍中有设定如下:当一个数组以pass-by-value的形式被传递给template function时,其类型总是被推衍为指针:1
f(name);//name is array,T deduced as const char*
但是我们需要明确,尽管函数的形参不能够真正地声明为数组类型,但其可以声明为数组的引用,因此假若我们有template function声明如下:1
2template <typename T>
void f(T& param);
我们再次传入数组,1
f(name);
此时T的类型将是一个真正的数组。数组类型包含了数组的长度,因此在本例中,T将会被推衍为const char[13],ParamType的类型为const char(&)[13]。这一特性也保证了模板可以推衍出数组包含元素的个数:1
2
3
4
5//return size of an array as complie-time constant
template<typename T,std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept{
return N;
}
constexpr意味着编译期间即可得到变量值,因此constexpr修饰的int形变量可以作为数组长度参数:1
2int keyVals[] = {1,3,7,9,11,22,35};
int mappedVals[arraySize(keyVals)];//此处使用std::array更佳
Function Arguments
数组名并非是C++中唯一会退化成指针的东西,函数名亦然,因此我们针对数组的所有讨论都可以应用于函数,举例而言:
1 | void someFunc(int,double);//type is void(int,double) |
好像在实际应用中没有什么差别,但知道总比不知道要好。
总结
- 在普通模板参数推衍过程中,reference-ness会被忽略。
- 对于universal reference,lvalue作为实参传入时会保留reference-ness。
- 当以pass-by-value形式类型推衍时,const-ness与volatile-ness会被忽略。
- 在模板类型推衍过程中,数组名与函数名会退化为指针,除非形参以引用形式得到了初始化。