Coding之路——重新学习C++(8):神奇的模板

1.解析一个正确的模板类

  (1)首先,我们想创造一个模板,可以先针对一个特定的类型参数设计它的行为方式,然后在对抽象的一般类型进行推广。例如我们可以先设计String<char>类的具体实现,然后再推广到String<C>类模板。

  (2)类模板的名字是不能重载的。所以,如果在某个作用域内声明了一个类模板,就不能有其他同样名字的实体了。

template<class T> class String{/*...*/};
class String {/*...*/};                                //错误:重复定义

  (3)在我们给模板实例化的时候,我们仅仅需要生成代码中我们用到的模板实例函数代码:

String<char> cs;

void f(){
    String <Jchar> js;
    cs = "Hello Word!";
}

这段代码中,只生成了String<char>和String<Jchar>的声明,和与它们对应的Srep类型,默认构造函数和析构函数,还有String<char>::operator=(char *),其他成员函数没有被使用,就不被生成。 

  (4)模板参数。模板参数可以使常量表达式,具有外部连接的对象或者函数地址,或者非重载的指向成员的指针。用做模板参数的指针必须具有&of的形式,其中of必须是对象或者函数的名字。到成员的指针必须有&X::of的形式,of是一个成员名。特别的,字符串常量不能作为模板参数。另外,一般把整型参数用于提供大小或界限:

template<class T, int i>class Buffer{
    T v[i];
    //...
};

  (5)类型检查。在模板定义里的名字必须在作用域里,或者以合理的明确的方式依赖于模板参数。另外,模板参数使用上的错误只有在模板使用时才能被检查出来。

  (6)模板一般采用分别编译,头文件中放入模板声明,原文件中将模板定义加上export关键字,让其他地方可以使用它。

2.函数模板

  (1)类型的自动推断。对于一个函数模板来说,编译器能够能够从一个调用中推断出类型参数和非类型参数,条件是函数调用的参数列表能唯一的标识类型参数的集合。但是编译器绝对不会对模板类自动推断类型,因为一个类可能有多个构造函数,这样自动推断就会不清楚具体的模板参数类型。在模板函数中经常会使用显式描述,显式描述的最常见用途是为模板函数提供返回值:

template<class T, class U> T implict_cast(U u){return u;}
int i = 1;
implict_cast<double>(i);           //T是double, U是int
impict_cast<char, double>(i);   //T是char,U是double

  (2)模板函数的解析规则,我们用一个具体的例子来理解:

template<class T> T sqrt(T t);
template<class T> complex<T> sqrt(complex<T> t);
double sqrt(double b);

complex<double> z;
sqrt(2);        //sqrt<int>(int)
sqrt(2.0);     //sqrt(double)
sqrt(z);        //sqrt<double>(complex<double>)

——找出能参与这个重载解析的一组函数模板的专门化。例如sqrt(z),产生出sqrt<double>(complex<double>)和sqrt<complex<double> >(complex<double>)。

——选择其中专门化程度最高的函数模板。这就意味着sqrt(z)选择sqrt<double>(complex<double>)。因为任何匹配sqrt<T>(complex<T>)都匹配sqrt<T>(T)。

——做重载解析,应该同时考虑常规函数。如果一个模板参数已经被自动推断出来,则不能通过类型转换来匹配常规函数。对于sqrt(2)来说,选择sqrt<int>(int)而不是           sqrt(double)。

——如果一个函数和一个专门化同样好,那么选择函数。sqrt(2.0)选择了sqrt(double)而不是sqrt<double>(double)。

——找不到匹配就是一个错误。要么通过显式调用消除歧义,要么增加适当的声明。

3.怎样用模板参数描述策略

  我们先来看一个排序实例。我们使用的排序规则基本一致,但是假如瑞典人的姓名排序就有些特殊, 这样我们就得另外制定一个排序规则,将它运用到函数模板中:

template<class T> class Cmp{
    static int eq(T a, T b){return a == b;}
    static int lt(T a, T b){return a < b;} 
    static int gt(T a, T b){return a > b;}   
};

class Literate{
       static int eq(char a, char b){return a==b;}
       static int lt(char, char){//...}               //基于字符值查一个表
};

template<class T, class C = Cmp<T> >
int compare(const String<T> &a, const String<T> &b){
     for(int i = 0;i < a.length() && i < b.length();i++){
        if(!C::eq(a[i], b[i])) return C::lt(a[i], b[i])? -1 : 1;
    return a.length() - b.length();
}

//调用
void f(String<char> a, String<char> b){
    compare(a, b);                  //用Cmp<char>
    compare<char, Literate>; //用Literate
}

在示例中,把比较操作作为模板参数传递有两个优点:一是可以通过一个参数传递几个操作,二是没有运行时的开销。

4.专门化

  在模板中,针对不同的模板参数我们可能需要不同的实现方式,这时我们就需要专门化来为我们制定特定类型的模板的实现方式。专门化针对共同界面,为不同的参数提供不同的实现方法。注意,模板本身和专门化必须在同一个命名空间中,通用模板必须在专门化模板之前声明,使用专门化的调用也必须在专门化模板的作用域。

下面这个例子就实现了对指针容器的专门化:

//通用模板
template<class T> class Vector{/*...*/};
//针对void*的模板专门化
template<> class Vector<void*>{
    void **p;
    //...
    void* operator[](int i );
};
//针对T*的部分专门化
template<class T> class Vector<T*>:private Vector<void*>{
public:
    typedef  Vector<void*> Base;
    Vector():Base(){}
    explicit Vector(int i):Base(i){}
    T*& elem(int i){return reinterpret_cast<T*&>(Base::elem(i));}
    T*& operator[](int i){
        return reinterpret_cast<T*&>(Base::operaotr[](i));
    }    
};

从一个非模板类派生出模板类,这是为一组模板提供共同实现的一种方法。

5.派生与模板

  (1)我们实现一个容器类,那如何将容器与操作分离呢,第一种方法是通过继承,父类模板参数类型是子类:

template<class T> class Base_ops{
public:
    bool operator==(const T&) const;
    bool operator!=(const T&) const;
    //给T操作访问的权限
    const T& derived() const{return static_cast<const T&>(*this);}
    //...
};

template<class T> Math_container:
    public Base_ops<Math_container<T> >{
    public:
        size_t size() const;
        T& operator[](size_t i);
        const T& operator[](size_t i) const;
        //...
};

第二种方法是将容器和操作通过模板参数组合起来:

template<class T, class C> class Mcontainer{
    C elements;
public:
    T& operator[](size_t i){return elements[i];}
    friend bool operator==(const Mcontainer&, const Mcontainer&);
    friend bool operator!=(const Mcontainer&, const Mcontainer&);
};

template<class T> class My_array{/*...*/};
Mcontainer<double, My_array<double> > mc;

  (2)一个类可以包含本身就是模板的成员:

template<class S>class complex{
    S re, im;
    template<class T> 
        complex(const complex<T> &c): re(c.real()),im(c.imag()){}   
    //...
};

complex<float> f(0,0);
complex<double> d = f; //可以:有float到double的转换

但是模板构造函数不会用于生成复制构造函数和赋值运算符,所以必须自己定义复制构造函数和赋值运算符。

  (3)同一个模板生成的两个类之间不存在任何关系。

class Shape{/*...*/};
class Circle:public Shape{/*...*/};
class Triangle:public Shape{/*...*/};

void f(set<Shape*> &s){
    s.insert(new Triangle());
}

void g(set<Circle*> &s){
    f(s);            //错误:类型呢不匹配,set<Circle*>不是set<Shape*>
}

我们必须保证set<Circle*>的成员一定是Circle,如果将set<Circle*>看成set<Shape*>,我们将不能得到保证,因为set<Shape*>允许插入Triangle类型,如果set<Shape*>实际是一个set<Circle*>,那么我们就摧毁了set<Circle*>的成员一定是Circle的基本保证。

  (4)下面的示例是告诉我们如何用指针模板反应出被指向对象的继承关系:

template<class T> Ptr{
    T *p;
public:
    Ptr(T *P);
    Ptr(const Ptr&);
    template<class T2> operator Ptr<T2>();
    //...
};

template<T>
    template<T2>
        Ptr<T>::operator Ptr<T2>(){ return Ptr<T2>(p);}

void f(Ptr<Circle> pc){
    Ptr<Shape> ps = pc;    //正确:Circle能转化为Shape
    Ptr<Circle> pc2 = ps;   //错误:Shape不能转化为Circle
}
原文地址:https://www.cnblogs.com/xskCoder/p/4003192.html