首页 > 代码库 > 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):神奇的模板