zoukankan      html  css  js  c++  java
  • 模板实参演绎

    一、什么是实参演绎

    如果我们每次都必须显式的指明模板替换参数类型,例如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,转载请注明出处。
  • 相关阅读:
    循序渐进学Python 1 安装与入门
    常用yum命令小结
    为CentOS配置网易163的yum源
    PHP合并数组+与array_merge的区别
    让Docker功能更强大的10个开源工具
    Docker入门系列8
    Docker入门系列7 动态映射端口port mapping
    e 的由来
    ROS教程5 使用串口
    1 ROS+ 使用ORB_SLAM2进行全场定位
  • 原文地址:https://www.cnblogs.com/claruarius/p/4084942.html
Copyright © 2011-2022 走看看