首页 > 代码库 > Coding之路——重新学习C++(8):神奇的模板

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

2.函数模板

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

template<class T, class U> T implict_cast(U u){return u;}int i = 1;implict_cast<double>(i);           //T是double, U是intimpict_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的基本保证。

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