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,转载请注明出处。
  • 相关阅读:
    English 2
    速算24点
    心理学1
    从微服务到函数式编程
    034 01 Android 零基础入门 01 Java基础语法 04 Java流程控制之选择结构 01 流程控制概述
    033 01 Android 零基础入门 01 Java基础语法 03 Java运算符 13 运算符和表达式知识点总结
    032 01 Android 零基础入门 01 Java基础语法 03 Java运算符 12 运算符和if-else条件语句的综合案例——闰年问题
    031 01 Android 零基础入门 01 Java基础语法 03 Java运算符 11 运算符的优先级
    030 01 Android 零基础入门 01 Java基础语法 03 Java运算符 10 条件运算符
    029 01 Android 零基础入门 01 Java基础语法 03 Java运算符 09 逻辑“非”运算符
  • 原文地址:https://www.cnblogs.com/claruarius/p/4084942.html
Copyright © 2011-2022 走看看