C++ 模板

来源:互联网 发布:linux配置bond模式1 编辑:程序博客网 时间:2024/05/16 11:38

1.类模板与模板类的概念

什么是类模板 一个类模板(也称为类属类或类生成类)允许用户为类定义一种模式,使得类中的某些数据成员、默写成员函数的参数、某些成员函数的返回值,能够取任意类型(包括系统预定义的和用户自定义的)。

  如果一个类中数据成员的数据类型不能确定,或者是某个成员函数的参数或返回值的类型不能确定,就必须将此类声明为模板,它的存在不是代表一个具体的、实际的类,而是代表着一类类。

类模板定义 定义一个类模板,一般有两方面的内容:

A.      首先要定义类,其格式为:

template <class T>

class foo

{

……

}

foo 为类名,在类定义体中,如采用通用数据类型的成员,函数参数的前面需加上T,其中通用类型T可以作为普通成员变量的类型,还可以作为const和static成员变量以及成员函数的参数和返回类型之用。例如:

template<class T>

class Test{

private:

   T n;

    constT i;

    staticT cnt;

public:

    Test():i(0){}

    Test(T k);

    ~Test(){}

    void print();

   T operator+(T x);

};

B.      在类定义体外定义成员函数时,若此成员函数中有模板参数存在,则除了需要和一般类的体外定义成员函数一样的定义外,还需在函数体外进行模板声明

例如

template<class T>

void Test<T>::print(){

    std::cout<<"n="<<n<<std::endl;

    std::cout<<"i="<<i<<std::endl;

    std::cout<<"cnt="<<cnt<<std::endl;

 

}

如果函数是以通用类型为返回类型,则要在函数名前的类名后缀上“<T>”。例如:

template<class T>

Test<T>::Test(T k):i(k){n=k;cnt++;}

template<class T>

T Test<T>::operator+(T x){

               return n + x;

               }

C.      在类定义体外初始化const成员和static成员变量的做法和普通类体外初始化const成员和static成员变量的做法基本上是一样的,唯一的区别是需在对模板进行声明,例如

template<class T>

int Test<T>::cnt=0;

template<class T>

Test<T>::Test(T k):i(k){n=k;cnt++;}

类模板的使用 类模板的使用实际上是将类模板实例化成一个具体的类,它的格式为:类名<实际的类型>。

  模板类是类模板实例化后的一个产物。说个形象点的例子吧。我把类模板比作一个做饼干同的模子,而模板类就是用这个模子做出来的饼干,至于这个饼干是什么味道的就要看你自己在实例化时用的是什么材料了,你可以做巧克力饼干,也可以做豆沙饼干,这些饼干的除了材料不一样外,其他的东西都是一样的了。

2.类模板的派生

  可以从类模板派生出新的类,既可以派生类模板,也可以派生非模板类。派生方法:

⑴ 从类模板派生类模板可以从类模板派生出新的类模板,它的派生格式如下例所示:

template <class T>

class base

{

……

};

 

template <class T>

class derive:public base<T>

{

……

};

与一般的类派生定义相似,只是在指出它的基类时要缀上模板参数,即base<T>。

⑵ 从类模板派生非模板类  可以从类模板派生出非模板类,在派生中,作为非模板类的基类,必须是类模板实例化后的模板类,并且在定义派生类前不需要模板声明语句:template<class>。例如:

template <class T>

class base

{

……

};

 

class derive:public base<int>

{

……

};

在定义derive类时,base已实例化成了int型的模板类。


断断续续地看了《C++Templates》很长时间,在叹服于模板的神奇之余,也不得不承认这是一块充满陷阱的湿地。每次想要动手神奇一把,都会被某编译错误给无情地挡回;看英文似的,水平不够,只能“读得懂,写不出”;成了饥饿的小白鼠,远处放着奶酪,近处却是一鼠夹。此次总结,希望把最近的模板实践都剖析出要点,避免再犯相同的错误。
模板的魅力

当前,对于多数C++程序员来说,模板常常意味着类型的简单替换。例如:

 
view plaincopy to clipboardprint?

    template<typename T>   
    T max(T const & a, T const & b)   
    {   
        return (a>b)?a:b;   
    }  

view plainprint?

    template<typename T>  
    T max(T const & a, T const & b)  
    {  
        return (a>b)?a:b;  
    }  

 

或 std::vector<T> 这样的用法。诚然,这是一种编程模式:一些类、函数、代码,宏观上的操作是相同的,不同的仅是被操作的类型,于是把类型特别提出来用一记号表示,并由template关键字声明,成为一通用的类、函数、代码;使用时,只要把特定的类型代替进去即可。在很多支持泛型的语言中,如C#、Java等等,模板就是这样用的。但是,读过《C++ Templates》或《Modern C++ Design》或《C++ Template Metaprogramming》的朋友都知道,C++的模板,包含的内容更多,功能也更为强大。

对于C++模板精髓,我想主要是SFINAE(Substitution failure is not an error)原则。SFINAE原则意味着,函数实例若不能匹配某个函数模板,这不算编译错误,还可以尝试去匹配其它函数模板。这个保护性的原则,提高了函数模板的重载能力,进而使得编译时类型推导成为可能。如下面的代码

 
view plaincopy to clipboardprint?

    #include <cstdio>   
      
    template<typename C> char check(int C::*);   
    template<typename T> float check(...);   
      
    template<typename T>   
    bool IsClassType() {   
        return sizeof(check<T>(0))==1 ;   
    };   
      
    class One;   
      
    int main()   
    {   
        printf("Is A Class Type %d/n",IsClassType<std::size_t>());   
        printf("Is A Class Type %d/n",IsClassType<One>());   
        return 0;   
    }   

view plainprint?

    #include <cstdio>  
      
    template<typename C> char check(int C::*);  
    template<typename T> float check(...);  
      
    template<typename T>  
    bool IsClassType() {  
        return sizeof(check<T>(0))==1 ;  
    };  
      
    class One;  
      
    int main()  
    {  
        printf("Is A Class Type %d/n",IsClassType<std::size_t>());  
        printf("Is A Class Type %d/n",IsClassType<One>());  
        return 0;  
    }  

 

对于模板template<typename C> char check(int C::*),只有当C类型为类时,才可能有int C::* 这样的类型(表示一个指针,指向C类的某int型成员),这样才能匹配此模板。而对于template<typename T> float check(...),可匹配任意类型的T。所以当check<int>(0)不能匹配前者时,它会匹配后者,从而使IsClassType能得到正确的结果。利用SFINAE原则,可以做很多类型推导:判断一个类型是否为某类型或某类类型(如判断T是否为class类型),类型是否相等(T1与T2是否为同一类型),类型选择(根据某类型,选择T1类或T2类),判断两类型是否有继承关系等等。

模板的另一重要特性为静态性。这点是指模板的实例化是编译时进行时,程序运行时,已经是模板实例的结果了。利用这一点,可以优化代码。如上面的程序中,计算IsClassType<One>(),实际上已经在编译时计算完毕了,在程序中,直接替代为true;而不是在程序运行进再根据T的类型,重新计算sizeof(check<T>(0))是否等于1。再看一个例子,这程序显示了模板的计算能力(当然,这些计算都是编译时进行了;程序运行时,就能直接输出结果了;具体原理说明见此):

 
view plaincopy to clipboardprint?

    #include <iostream>    
      
    template<int N>    
    class Pow3 {    
      public:    
        enum { result=3*Pow3<N-1>::result };    
    };    
      
    template<>    
    class Pow3<0> {    
      public:    
        enum { result = 1 };    
    };    
      
    int main()    
    {    
        std::cout << "Pow3<7>::result = " << Pow3<7>::result    
                  << '/n';    
    }   

view plainprint?

    #include <iostream>   
      
    template<int N>   
    class Pow3 {   
      public:   
        enum { result=3*Pow3<N-1>::result };   
    };   
      
    template<>   
    class Pow3<0> {   
      public:   
        enum { result = 1 };   
    };   
      
    int main()   
    {   
        std::cout << "Pow3<7>::result = " << Pow3<7>::result   
                  << '/n';   
    }  

 

 
 模板的分类与使用

模板分为函数模板和类模板,有着显著的不同!不能因为两者都能进行类型替换(最主要的能力),而认为两者都差不了多少。在细节上,正是这些差异,使我写出模板常常不能顺利地通过编译器编译。

函数模板,一般的形式为:

template<typname  T>

RetType fun(T t);

而类模板,一般形式为:

template<typename T>

class MyClass;

函数模板的模板参数,不能有默认值(目前标准是这样),而类模板可以有;函数模板可以重载,重载的方式很多;但类模板是不能重载的,只能有一个基本模板。

对于第一个差异,常常出现的问题是,使用多了stl::vector<T, Allocate = std::allocate<T> >,也认为函数模板也能带默认的参数。而事实是,目前的标准里不允许这样,虽然编译器实现这个功能并不困难;而对待这样的语法错误,不同的编译器会给出不同的信息,其中VC给的信息非常不助于解决问题----“默认模板参数应该是出现在类模板中”,gcc则直接指出了“函数模板不能有默认参数”。

对于第二个差异,实际上是体现了类模板与函数模板发挥作用的不同方式:函数模板走的是“重载路线”,而类模板走的是“特化路线”。这两种方式所体现的,正是C++模板不同于普通泛型(类型替换)的特别之处。

 
重载函数模板

函数模板实际表式的是一系列逻辑相似的函数族。例如前面的T max(T const&, T const &)函数模板。当代码上求max(1,2)时,编译器根据参数1,2的类型,推断出T为int,从而产生了一个函数int max(int const &, int const&)。这个过程叫做函数模板的实参演绎。当出现函数模板重载的情况时,如:

 
view plaincopy to clipboardprint?

    #include <cstdio>   
      
    template<typename T>   
    int fun(T)    
    {   
        return 1;   
    }   
      
    template<typename T>   
    int fun(T*)    
    {   
        return 2;   
    }   
      
    int main()   
    {   
        int a=10;   
        printf("%d/n",fun(a));   
        printf("%d/n",fun(&a));   
        return 0;   
    }   

view plainprint?

    #include <cstdio>  
      
    template<typename T>  
    int fun(T)   
    {  
        return 1;  
    }  
      
    template<typename T>  
    int fun(T*)   
    {  
        return 2;  
    }  
      
    int main()  
    {  
        int a=10;  
        printf("%d/n",fun(a));  
        printf("%d/n",fun(&a));  
        return 0;  
    }  

 

输出结果为1,2。这个例子表明,对于函数模板,编译器会根据参数的类型,去选择最适合的函数。这个“最适合”往往是对参数限制得最多的一种选择。这也是之前对于类类型的One,check函数模板会被选择为char check(int C::*)的原因。

再举一个详细点的例子:对于自定义的list和vector两种数据结构,两者有基本相同的外部接口;但前者由链表实现,后者由数组实现,能支持随机访问;具体说来,这两者使用的迭代器不同,一个是bi_iterator(双向访问),一个是random_iterator(随机访问)。因此,要对list进行排序,要使用一种支持双向迭代器的方法link_sort(),而对于vector有更高效的quick_sort(),它仅支持随机访问迭代器。现在希望能统一接口,实现一sort函数,可以接受list和vector两种类型(stl里还有其它类型)的容器。代码如下:

 
view plaincopy to clipboardprint?

    class bi_iterator;   
    class random_iterator;   
      
    class list   
    {   
    public:   
        typedef bi_iterator iterator;   
        iterator begin();   
        iterator end();   
    };   
      
    class vector   
    {   
    public:   
        typedef random_iterator iterator;   
        iterator begin();   
        iterator end();   
    };   
      
    template<typename container>   
    void sort(container &c)   
    {   
       typename container::iterator tag;   
       _sort_select(c, tag);   
    }   
      
    template<typename container>   
    void _sort_select(container &c, bi_iterator & tag)   
    {   
       link_sort(c.begin(), c.end());   
    }   
      
    template<typename container>   
    void _sort_select(container &c, random_iterator & tag)   
    {   
       quick_sort(c.begin(), c.end());   
    }   

view plainprint?

    class bi_iterator;  
    class random_iterator;  
      
    class list  
    {  
    public:  
        typedef bi_iterator iterator;  
        iterator begin();  
        iterator end();  
    };  
      
    class vector  
    {  
    public:  
        typedef random_iterator iterator;  
        iterator begin();  
        iterator end();  
    };  
      
    template<typename container>  
    void sort(container &c)  
    {  
       typename container::iterator tag;  
       _sort_select(c, tag);  
    }  
      
    template<typename container>  
    void _sort_select(container &c, bi_iterator & tag)  
    {  
       link_sort(c.begin(), c.end());  
    }  
      
    template<typename container>  
    void _sort_select(container &c, random_iterator & tag)  
    {  
       quick_sort(c.begin(), c.end());  
    }  

 

这同样是函数重载的例子,编译器根据_sort_select()的第二个参数tag,确定调用的是那一个函数模板,再生成相应的函数实例。上面的代码中,tag的值是多少其实并不重要,有用的是tag的类型,编译器据此来判断选择哪一个函数模板。编译后,tag被优化掉,并没有在程序中占用空间。(注意:typename container::iterator tag这一句里的关键字typename是用于强调iterator是模板参数container里的一个类型,避免一些语法上的起义发生。)

当然,为统一接口,还可以给list,vector都添加sort()方法。
特化类模板

特化是指通使用指定的模板实参来特殊化一个类,目标常常是优化基于某些类型的类实现。

例如

 
view plaincopy to clipboardprint?

    template <typename T, int N> // 基本模板   
    stack   
    {   
    public:   
        void push(T const & t);   
        T pop();   
        T const & top();   
    private:   
        T elements[N];   
    };   
      
    template<int N>   
    stack<string, N>           // 局部特化   
    {   
    public:   
        void push(string const & t);   
        string pop();   
        string const & top();   
    private:   
        string elements[N];   
    };   

view plainprint?

    template <typename T, int N> // 基本模板  
    stack  
    {  
    public:  
        void push(T const & t);  
        T pop();  
        T const & top();  
    private:  
        T elements[N];  
    };  
      
    template<int N>  
    stack<string, N>           // 局部特化  
    {  
    public:  
        void push(string const & t);  
        string pop();  
        string const & top();  
    private:  
        string elements[N];  
    };  

 

stack<T,N>是一个栈的类模板,T表示元素的类型,N表示栈的大小(整数可以做为模板参数)。对于一个字符串string的栈,我们期待更优化的实现。所以可以把T特化成具体的类型string,重新实现此模板(包括方法和成员)。由于这里只特化了一个模板参数,因此叫局部特化。

前面编译时计算Pow3的例子,也是类模板特化的例子:计算是递归进行的,计算Pow3<N>::result,需要先得到Pow3<N-1>::result;而最后的边界条件是通过特化得到的,即Pow3<0>::result=1。

下面再看一个特化的例子:

 
view plaincopy to clipboardprint?

    template<typename T1, typename T2> // 基本模板   
    struct IsSameType   
    {   
        enum {result = false};   
    };   
      
    template<typename T>   
    struct IsSameType<T,T>            //特化为T1=T2=T   
    {   
        enum {result = true};   
    };   

view plainprint?

    template<typename T1, typename T2> // 基本模板  
    struct IsSameType  
    {  
        enum {result = false};  
    };  
      
    template<typename T>  
    struct IsSameType<T,T>            //特化为T1=T2=T  
    {  
        enum {result = true};  
    };  

 

此例中:IsSameType<T1, T2>::result 的值表征了T1,T2两个类型是否为同一类型。

如果基本模板的成员不完整,而特化的模板却是完整的类模板,这样又能起另一作用:

 
view plaincopy to clipboardprint?

    #define StaticAssert(X)     /   
    {                           /   
       Assert< (X) > assert;    /   
    }                          
      
    template<bool condition>   
    struct Assert;   
      
    template<>   
    struct Assert<true>   
    {   
    };   

view plainprint?

    #define StaticAssert(X)     /  
    {                           /  
       Assert< (X) > assert;    /  
    }                         
      
    template<bool condition>  
    struct Assert;  
      
    template<>  
    struct Assert<true>  
    {  
    };  

 

可以看到,类模板template<bool condition>struct Assert只有声明(bool值也可以做为模板参数的实参),没有定义,而struct Assert<true>是有定义的。这就意味着,如果定义变量 Assert<false> v,则会得到一个编译错误。利用这种有缺陷的类模板,我们得到了一个静态断言StaticAssert。组合上面的代码,我们可以写出这样一个max的函数模板:

 
view plaincopy to clipboardprint?

    template<typename T>   
    T max(T & t1, T & t2)   
    {   
        StaticAssert( !IsClassType<T>() )   
      
        return (t1>t2)?t1:t2;   
    }   

view plainprint?

    template<typename T>  
    T max(T & t1, T & t2)  
    {  
        StaticAssert( !IsClassType<T>() )  
      
        return (t1>t2)?t1:t2;  
    }  

 

这个max(),要求T类型不能是类类型。因为若T为类,t1,t2可能是大对象,像代码中那样直接返回大对象是非常低效的。

注意:函数模板也可以特化,这与类模板特化是基本一致的。但在强大的实参演绎机制与重载机制前面,函数模板的特化往往不被提及。


原创粉丝点击