函数模板大致形如:
template<typename T>
void f(ParamType param);
而一次调用形如:
f(expr); //以某表达式调用f
在编译期,编译器会通过expr推导两个型别:一个是T的型别,另一个是ParamType的型别,这两个型别往往不一样。因为ParamType常会包含一些饰词,如const或引用符号限定词。例如模板声明如下:
template<typename T>
void f(const T& param); //ParamType 是 const T&
而调用语句如下:
int x = 0;
f(x); //以一个int 调用f
在此例中,T被推导为int,而ParamType则被推导为const int&.
我们很自然地会认为,T的型别推导结果和传递给函数的实参类型是同一的。换句话说,T的型别就是expr的型别。在上例中,情况确乎如此:x的型别是int,T的型别也推导为int,但是,这一点并不总是成立。T的型别推导结果,不仅仅依赖expr的型别,还依赖ParamType的形式。具体要分三种情况讨论:
- ParamType具有指针或引用型别,但不是个万能引用
- ParamType是一个万能引用
- ParamType既非指针也非引用
我们仍采用前述模板和调用的一般形式。
template<typename T>
void f(const T& param); //ParamType 是 const T&
情况1:paramType是个指针或引用,但不是个万能引用
最简单的莫过于当ParamType是个指针或引用,但不是万能引用的情形了,在这种情况下,类型推导会这样运作:
- 若expr具有引用型别,先将引用部分忽略
- 尔后,对expr的型别和ParamType的型别执行模式匹配,来决定T的型别。
例如,我们的模式如下:
template<typename T>
void f(T& param); //param是个引用
又声明了下列变量:
int x = 27; //x的型别是int
const int cx = x; //cx的型别是const int
const int& rx = x; //rx是x的型别为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对象是安全的:该对象的常量性(constness)会成为T的类型推导结果的组成部分。
在第三个调用中,请注意,即使rx具有引用型别,T也并未被推导成一个引用。原因在于,rx的引用性(reference-ness)会在类型推导过程中被忽略。
尽管上述调用语句示例演示的都是左值引用形参,但是右值引用形参的型别推导运作方式是完全相同的。当然,传给右值引用形参的,只能是右值引用实参,但这个限制和型别推导无关。
如果我们将形参型别从T&改为const T&,结果会有一点变化,但这些变化并没有什么出乎意料之处。cx和rx的常量性仍然得到了满足,但是由于我们现在会假定param具有const引用型别,T的型别推导结果中包含const也就没有必要了。
template<typename T>
void f(const T& param); //param现在是个const引用了
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是个指针(或指涉到const对象的指针)而非引用,运作方式本质上并无不同:
template<typename T>
void f(T* param); //param现在是个指针了
int x = 27; //同前
const int *px = &x; //px是指涉到x的指针,型别为const int
f(&x); //T的型别是int, param的型别是int*
f(px); //T的型别是const int, param的型别是const int*
情况2:ParamType是个万能引用
对于持有万能引用形参的模板而言,规则就不那么明显了,此类形参的声明方式类似右值引用(即在函数模板中持有型别形参T时,万能引用的声明型别写作T&&),但是当传入的实参是左值时,其表现会有所不同。
- 如果expr是个左值,T和ParamType都会被推导为左值的引用。这个结果具有双重的奇特之处:首先,这是在模板型别推导中,T被推导为引用型别的唯一情形。其次,尽管在声明时使用的是右值引用语法,它的型别推导结果确实左值引用。
- 如果expr是个右值,则引用:常规(即情况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&&
万能引用形参的型别推导规则不同与左值引用和右值引用形参。具体地,当遇到万能引用时,型别推导规则会区分实参是左值还是右值。而非万能引用时从来不会作这样的区分的。
情况3:ParamType既非指针也非引用
当ParamType既非指针也非引用时,我们面对的就是所谓按值传递了:
template<typename T>
void f(T param); //param现在是按值传递
这意味着,无论传入的是什么,param都会是它的一个副本,也即是一个全新对象。param会是个全新对象这一事实促成了如何从expr推导出T的型别的规则:
- 一如之前,若expr具有引用型别,则忽略其引用部分
- 忽略expr的引用性之后,若expr是个const对象,也忽略之。若其是个volatile对象,同忽略之
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的一个副本。从而cx和rx不可修改这一事实并不能说明param是否可以修改。正是由于这一原因,expr的常量性以及挥发性(volatileness,若有)可以在推导param的型别时加以忽略:仅仅由于expr不可修改,并不能断定其副本也不可修改。
需要重点说明的是,const(和volatile)仅会在按值形参处被忽略。正如此前所见,若形参是const的引用或指针,expr的常量性会在型别推导过程中加以保留。但是考虑这种情况:expr是个指涉到const对象的const指针,且expr按值传给param:
template<typename T>
void f(T param); //param仍按值传递
const char* const ptr = "Fun with pointers"; //ptr是个指涉到const对象的const指针
f(ptr); //传递型别为const char* const的实参
这里,位于星号右侧的const将ptr声明为const,ptr不可以指涉到其他内存位置,也不可被置为null.可ptr被传递给f时,这个指针本身将会按比特复制给param。换言之,ptr这个指针会被按值传递。依照按值形参的类别推导规则,ptr的常量性会被忽略,param的型别会被推导为const char*,即一个可修改的,指涉到一个const字符串的指针;在型别推导的过程中,ptr指涉到的对象的常量性会得到保留,但其自身的常量性则会在以复制方式创建新指针param的过程中被忽略。
数组实参
数组型别有别于指针型别,尽管有时候它们看起来可以互换,形成这种假象的主要原因是,在很多语境下,数组会退化成指涉到其首元素的指针。下面这段代码之所以能够通过编译,是因为这种退化机制在发挥作用:
const char name[] = "J.P.Briggs"; //name的型别是const char[13]
const char * ptrToName = name; //数组退化成指针
这里,型别为const char* 的指针ptrToName是通过name来初始化的,而后者的型别是const char[13].这两个型别(const char* 和 const char[13])并不统一,但是因为数组到指针的退化规则的存在,上述代码能够通过编译。
但当一个数组传递给持有按值形参的模板时,又会怎么样呢?
template<typename T>
void f(T param); //持有按值形参的模板
f(name); //T和param的型别会被推导成什么呢?
我们是先观察到,并没有任何的函数形参具有数组型别。没错,下面的语法是合法的:
void myFunc(int param[]);
但是既然数组声明可以按照指针声明的方式加以处理,那就意味着myFunc可以等价地声明如下:
void myFunc(int* param);
这种数组和指针形参的等价性,是作为c++基础的C根源遗迹。它使得“数组和指针型别是一回事”这一假象愈加扑朔迷离。
由于数组形参声明会按照它们好像是指针形参那样加以处理,按值传递给函数模板的数组型别将会被推导成指针型别。也就是说,在模板f的调用中,其型别形参T会被推导成const char*.
f(name); //name是个数组,但T的型别却被推导成const char*
尽管函数无法声明真正的数组型别的形参,它们却能够将形参声明成数组的引用!所以,如果我们修改模板f,指定按引用方式传递其实参,
template<typename T>
void f(T& param); //按引用方式传递形参的模板
然后,向其传递一个数组,
f(name); //向f传递一个数组
在这种情况下,T的型别会被推导成实际的数组型别!这个型别中会包含数组尺寸,在本例中,T的型别推导结果是const char[13],而f的形参(该数组的一个引用)型别会被推导为const char(&)[13].
可以利用声明数组引用这一能力创造出一个模板,用来推导出数组含有的元素个数:
//以编译器常量形式返回数组尺寸
//该数组未起名字,因为我们只关心其含有的元素个数
template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept
{
return N;
}
将该函数声明为constexpr,能够使得其返回值在编译期就可用,从而就可以在声明一个数组时,指定其尺寸和另一个数组相同,而后者的尺寸则从花括号初始化计算得出:
int keyVals[] = {1, 3, 4, 5,7,8}; //含有6个元素
int mappedVals[arraySize(keyVals)]; //mappedVals被指定与之相同
函数实参
数组并非C++中唯一可以退化为指针之物,函数型别也同样会退化成函数指针。并且我们针对数组相别推导的一切讨论都适用于函数及其想函数指针的退化。所以结果如下:
void someFunc(int, double); //函数,其型别为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);
要点速记:
- 在模板型别推导过程中,具有引用型别的实参会被当成非引用型别来处理。换言之,其引用性会被忽略。
- 对万能引用形参进行推导时,左值实参会进行特殊处理
- 对按值传递的形参进行推导时,若实参型别中带有const或volatile修饰粗,则它们还是会被当做不带const或volatile修饰词的型别来处理。
- 在模板型别推导过程中,数组或函数型别的实参会退化成对应的指针,除非它们被用来初始化引用