如果我们在一个function template调用语句中明确指定template arguments,会使代码变得笨拙而又不简洁,C++在设计时候考虑到了这种情况,提供了自变量推导的方法,便于我们书写代码,下面我们针对Template自变量推导机制进行必要的讨论。
推导过程
编译器会比对“调用语句内某自变量的类型”和“function template中对应的template parameters类型”,并试图总结出“被推导的一或者多个参数”的正确替换物。每一对“自变量-参数”独立分析,如果推导结束后两者结果有差异,则推导失败。参考如下代码:
template <typename T>
T const& max(T const& a,T const& b){
return a<b?b:a;
}
int g=max(1,1.0);
显然出错,因为T首先被推导为int,其次又被推导为double,出现歧义,因此推导失败。
即使所有的template parameters推导结果前后一致,如果以自变量替换进去后导致声明的其他部分非法,推导过程仍旧失败,如返回值不匹配,参考如下代码:
template <typename T>
typename T::ElementT at(T const& a,int i){
return a[i];
}
void f(int *p){
int x=at(p,7);
}
这里T被退到为int*,然而却推导失败,主要在于返回类型T::ElementT非法。
我们还需要探索“自变量-参数”匹配是如何进行的,因此我们声明A为template arguments,P为template parameters,如果参数被声明为by reference传递,P表示reference类型,A为自变量类型;否则P表示声明之参数类型,A是个由array或者function退化而成并去除外围const和volatile饰词的pointers指针类型,如:
template <typename T>
void f(T);
template <typename T>
void g(T&);
double x[20];
int const seven=7;
f(x);//nonrefernce parameter:T为double
g(x);//reference parameter:T为double[20]
f(seven);//nonreference parameter:T为int
g(seven);//reference parameter:T为int const
f(7);//nonreference parameter,T为int
g(7);//ERROR:不能将7当做int&传递
当字符串字面常数类型的自变量被绑定至reference parameters时,不会发生退化,参考如下代码:
template <typename T>
T const& max(T const& a,T const& b);
max("Apple","Pear");//ERROR
在上面代码中我们针对两种string类型的参数调用max时候,T并不会退化为char const*,奇怪的是“Apple”类型是char const[6],而“Pear”类型被推导为char const[5]。array至pointer类型的退化并没有发生,因为max的参数使用by-reference方式传递参数。
推导之前后脉络
复杂参数化类型的推导,可以参考如下例子进行简单了解:
template <typename T>
void f1(T*);
template <typename E,int N>
void f2(E(&)[N]);
template <typename T1,typename T2,typename T3>
void f3(T1 (T2::*) (T3*));pointer-to-member type
class S{
public:
void f(double*);
};
void g(int*** ppp){
bool b[42];
f1(ppp);//T被推导为int**
f2(b);//E被推导为bool,N被推导为42
f3(&S::f);//推导结果:T1=void,T2=S,T3=double
}
复杂的类型声明是由基本构件(pointer、reference、array、function声明符号、pointer-to-member声明符号、template-id等等)构件而成,匹配过程从上层构件开始,在各组成元素中递归进行。公正地说,大多数类型声明构件都可以使用这种方式进行匹配,这些构件被称为deduced contexts。但是相应的有些构件并不是deduced contexts:
受饰类型名称。例如Q<T>::X不会被用以推导template parameter T。例如S
template <int N>
class X{
public:
typedef int I;
void f(int){
}
};
template <int N>
void fppm(void (X<N>::*p) (typename X<N>::I));
int main(){
fppm(&X<33>::f);//OK,N被推导为33
return 0;
}
在function template fppm()中,子构件X<N>::I是个nondeduced context。然而pointer-to-member类型中的X<N>却是个deducible context,而且当由此导出的参数N被塞入nondeduced context时,我们将获得一个与实元&X<<33>33>::f相容的类型。于是“自变量-参数”推导成功。
对一个完全伊deduced contexts构建而成的参数类型进行推导,反而导致矛盾结果,参看如下代码,前提是class templates X和Y都已被正确声明:
template <typename T>
void f(X<Y<T>,Y<T> >);
void g(){
f(X<Y<int>,Y<int> >());//OK
f(X<Y<int>,Y<char> >());//ERROR
}
第二个function template f()的调用语句的问题在于,根据两个自变量推导出不同的template parameter T,这是不合法的。上述两种情况中,call arguments都是由class template X的default都早函数建立的一个咱叔对象。
特殊推导情境
有两种情形使得(A,P)并非由function template的调用自变量和function template的参数获得。
1)对function template进行取址时,这时P是function template声明语句中的参数化类型,A是被初始化或者被赋值之指针的函数类型。例如:
template <typename T>
void f(T,T);
void (*pf)(char,char)=&f;
这个例子中,p是void(T,T),A是void(char,char),推导成功,并将T替换为char,pf被初始化为f<char>特化体的地址。
2)conversion operator templates(转型运算符模板):
class S{
public:
template<typename T,int N> operator T&();
};
这时(P,A)的获得犹如涉及自变量及某参数类型,下面例子展示了上述情况的一个变异:
void f(int(&)[20]);
void g(S s){
f(s);
}
这里我们把s转型为int(&)[20]。因此类型A为int[20],类型P为T。推导成功,T被替换为int[20]。
可接受的自变量转型
通常,template的推导机制会视试图寻找function template parameters的替换品,使参数化类型P与类型A完全相同。然而当它无法达到这个目标时,推导机制容许以下差异:
如果原始函数使用by-reference传递方式,责备替换类型P被类型A可以多带上const饰词或volatile饰词;
如果类型A是一个pointer类型或者pointer-to-member类型,A可以通过一个const饰词或volatile饰词,转型为替换类型P;
除非推导程序因conversion operator template而发生,否则被替换类型P可以使类型A的base class类型,或者A是个pointer-to-X类型时,P可以是个pointer-to-base-class-of-X,参看如下代码:
PS:derived-to-base不能适用于nontype template parameters
template <typename T>
class B{};
template <typename T>
class D:public B<T>{
};
template <typename T>
void f(B<T>*);
void g(D<long> dl){
f(&dl);//OK
}
当推导机制无法进行完全匹配时,才会考虑这种宽松的匹配条件。即使如此,唯有当推导机制找到唯一一个(使类型A复合被替换类型P)的替换方式,推导才会成功。
Class Template Parameters(类别模板参数)
Template argument deduction专门只用于function template和member function template中,更名却的说,class template的自变量不从外界对其中某一构造函数的call arguments中推导出来。
template <typename T>
class S{
public:
S(T b):a(b){
}
private:
T a;
};
S x(12);//ERROR:class template parameter T从来不从构造函数中被推导出来
预设的调用自变量(Default Call Arguments)
在function template中你也可以指定预设的函数调用自变量,就像在常规函数中一样:
template <typename T>
void init(T* loc,T const& val=T()){
*loc=val;
}
事实上,正如这个例子所战术的,预设的function call argument也可以依赖某个template parameter。这种受控预设自变量只在调用段没有明确自变量时才被实例化。正是这个法则使得下面的例子合法:
class S{
public:
S(int,int);
};
S s(0,0);
int main(){
init(&s,S(7,42));//T=S时T()不合法,但由于给定了一个明确自变量,因此不需将预设自变量T()实例化
return 0;
}
即使预设的function call arguments并非受控的,它也无法被用来推导template arguments。所以下面的例子不合法:
template <typename T>
void f(T x=42){
}
int main(){
f<int>();
f();//ERROR:无法从预设的call argument中推导T
return 0;
}
Barton-Nackman Trick
由于以前function template无法重载,所伊针对需要重载的成员函数,Barton和Nackman解决该问题的方法是:把这些需要重载的函数声明为class内部的一个普通的friend函数,举例如下:
template <typename T>
class Array{
public:
...
friend bool operator==(Array<T> const&a,Array<T> const&b){
return ArrayAreEqual(a,b);
}
};
函数本身不是function template的局纤体,只是个普通的non-template function,因具现过程影响而插入global作用域中,由于是个non-template function,可以和其他重载形式的operator==共存。