模板实参演绎

一、什么是实参演绎

如果我们每次都必须显式的指明模板替换参数类型,例如concat<std::string, int>(s, 3),那么过程将会显得非常繁琐。
如果我们可以concat(s, 3)//之前必须声明s是std::string类型,那么将会向普通的函数调用一样简单,
事实上,C++是允许这样的写法,然后C++编译器会根据实参(s和3)的类型推算出合适的替换类型。
 
如何演绎?
1 template<typename T>
2 T const& max(T const& a T const& b)
3 {
4     return a < b ? b : a;
5 }
6 
7 int g = max(1, 1.0);
上面的演绎会失败,为什么?因为“函数模板对应的参数化类型T”只有一种,但是“调用实参类型”(1和1.0的类型为int型和double型),会导致演绎矛盾,所以失败。
另一种失败的情况是替换的类型导致一个无效的构造。如下:
 1 template<typename T>
 2 typename T::ElementT at(T const& a, int i)
 3 {
 4     return a[i];
 5 }
 6 
 7 void f(int* p)
 8 {
 9     int g = at(p, 1);// 这里会演绎失败,因为T被演绎成T*,但是int*没有一个叫做ElementT的替换类型。
10 }
二、灵活的替换规则  
 为了方便我们下面的说明,我们来约定
来自调用实参类型 的类型为匹配类型A
来自函数模板对应的参数化类型T 的类型为匹配类型P
(1)如果被声明的参数 是一个带引用的声明(T& 或 T const&),那么匹配类型P(就是T 或 T const), A仍然是实参类型,不会忽略高层次的const和volatile限定符号。
(2)如果被声明的参数 是一个非引用的声明,P就是所声明的参数类型(比如T*),A仍然是实参类型, 同时还会忽略高层次的const和volatile限定符号。
(3)如果被声明的参数 是一个非引用的声明,如果这个实参是数组或者是函数类型(函数指针),那么还会发生decay转换,转换为对应的指针类型,同时还会忽略高层次的const和volatile限定符号。
(4)实参7不能传递给int&
template<typename T> void f(T); //P是 T

template<typename T>void g(T&); //P是T

double x[20];

int const seven = 7;

f(x); //T被演绎成double*,decay转化
g(x);//T被演绎成double[20]
f(seven);//T被演绎成int,忽略了高层的const
g(seven);//T被演绎成int const,不会忽略
f(7);//T被演绎成int
g(7);//T被演绎成int,但是7不能传递给int&

(5)又一个坑:实参为字符串,那么演绎的结果T应该是字符串数组,不是字符串指针char*

template<typename T>
T const& max(T const& a, T const& b);

max("Apple", "Pear");“Apple”的类型是char const[6], “Pear”的类型是char const[5];而且不存在数组到指针的decay转型(因为演绎的参数是引用参数)。因此为了演绎成功,T就同时必须得是char[6]和char[5]。这显然是不可能的,所以产生错误。

三、练手

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*));

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
}
演绎上下文的过程:匹配从最顶层开始,然后不断的递归各种组成元素。
然而有两类构造不能用于演绎上下文。
(1)受限的类型名称,Q<T>::X的类型名称不能用来演绎模板参数T
(2)除了非类型参数外,模板参数还包含其他的成分的非类型表达式,诸如S<I+1>,int(&)[sizeof(S<T>)]类型不能用来演绎I和T
为什么?因为演绎过程并不唯一(甚至不一定有限的情况内演绎完毕)。
template<int N>
class X{
public:
    typedef int I;
    void f(int){}
};

现在如果用 一个int 演绎typename X<N>::I ,那么是不成功的,为什么因为我可以有很多X<1>::i,X<2>::I, X<3>::I …… X<1000>::I,表示,所以C++禁止这种演绎。

template<int N>
void fppm(void (X<N>::*p)(typename X<N>::I));

int main()
{
    fppm(&X<33>::f);//可以演绎成功,N=33
}
在函数模板fppm()中,子构造X<N>::I不是一个可以演绎的上下文,但是可以通过成员指针类型(X<N>::*p)的成员部分X<N>演绎上下文。
四、特殊情况的演绎
存在两种情况,其中演绎实参-参数对(A,P)并不是分别来自函数调用的实参,函数模板参数。
template<typename T>
void f(T, T);

void (*pf)(char, char) = &f;
(1)如上,第一种情况是取函数模板的地址的时候, A就是void(char, char), P就是void(T, T),
T被演绎成char,同时pf被初始化为“特化f<char>“的地址
------------------------我是华丽的分割线╮( ̄▽ ̄")╭------------------------------------
class S{
public:
    template<typename T, int N> operator T[N]&(); //我是转型运算符,转换成T[N]类型
};

void f(int(&)[20]); //参数的类型是20个元的数组

void g(S s)
{
    f(s);
}
我们试图把S转换成int(&)[20];因此类型A是int(&)[20], 类型P为T[N],于是用int替换T,用20替换N。演绎类型就是成功的。
 
五、再谈灵活的替换规则
(1)对于模板参数-实参对(P, A),有两种情况下,P可以比A多一个const或volatile限定符。
①原来声明的参数是一个引用参数子,那么P类型可以比A类型多出一个const或volatile
②原来的声明参数是不是一个引用参数子没关系,A类型是指针或成员指针类型,那么P类型也可以比A类型多出一个const或volatile。
(2)当演绎过程不涉及转型运算符模板时,被替换的P类型可以是A类型的基类,或者是当A是指针类型时,P可以是一个指针类型,P指向的类型是A所指向类型的基类, 只有在不精确匹配情况下才会出现这中宽松匹配。
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); //成功,用long替换T
}
六、警告:类模板参数不能用于实参演绎
 
七、警告:函数模板的缺省实参不能用于实参演绎,即使实参不是依赖型的实参
template<typename T>
void f(T x = 42)
{}

int main()
{
   f<int>();  //正确,实例化
   f();//错误,不能用于实参演绎
}
八、一个小技巧 Barton-Nackman方法
当时这种方法被创建出来基于以下几个原因:
(1)当时函数模板不能被重载
(2)运算符==如果重载在类模板里面那么,根据上面的那些灵活的转换方式(指向基类,指向子类之云云),第一个实参(this指针指向),第二个实参的转型规则可能不一样。
现在定义一个模板类Object,那如果要定义这个类的operator ==,那么这个operator==不能定义在类内部(根据(2)),
也不能定义在全局或类之外的命名空间,如template<typename T> bool operator ==(Array<T> const& a, Array<T> const& b){……},(根据(1))
 
Barton和Nackman将这个运算符作为类的普通友元函数定义在类的内部。如下
#include <iostream>
using namespace std;

template<typename T>
class Object{
    public:
    int a;
    Object(int n):a(n){}
    friend bool operator == (Object<T> const&lt, Object<T> const& rt)
    {
        return equal(lt, rt);    //根据参数类型调用重载的函数
    }
};

bool equal(Object<int> const& lt, Object<int> const& rt) //这是一个普通函数,可以被随便重载
{
    return lt.a == rt.a;
}

int main()
{
    Object<int> s(1);
    Object<int> s1(1);
    cout << (s == s1) << endl;
    return 0;
}

最后顺利编译通过,运行成功。

注:这些代码的运行环境都是在mingw下进行的,VS2013估计自己重新实现了模板名字查找,很多书上说名称找不到的情况,VS2013都找得到(-__-)b,所以为了更好的学习《C++Templates》转投MinGW,编辑器是codeblocks。
 
编辑整理:Claruarius,转载请注明出处。
posted @ 2014-11-09 12:42  Claruarius  阅读(1003)  评论(0编辑  收藏  举报