詳解c++中的trait與policy模板技術

概述

我們知道,類有屬性(即數據)和操作兩個方面。同樣模板也有自己的屬性(特別是模板參數類型的一些具體特征,即trait)和算法策略(policy,即模板內部的操作邏輯)。模板是對有共性的各種類型進行參數化後的一種通用代碼,但不同的具體類型又可能會有一些差異,比如不同的類型可能會有自己的不同特征和算法實現策略。

trait模板技術

當在模板代碼中需要知道類型參數T的某些特征(比如需要知道T是哪個具體類型,是否有默認構造函數,希望該類型有合理的缺省值,如int型缺省值為0),我們可以聲明一個描述T的特征的trait<T>模板,然後對每種具體類型(如int,char,用戶定義的類)特化trait<T>,在各特化版本中用typedef為該具體類型(或者想映射成的其他類型)定義統一的別名(比如AliT),根據需要還可指定合理的缺省值等。這樣在原來模板文件中#include這個trait<T>模板的文件,就可以在模板代碼中使用trait<T>::AliT來獲得T的具體特征。

比如我們要計算數組各個元素的累加和,由於數組元素可以是各種類型,我們使用模板來實現它,這時有一個類型參數T。但在算法代碼中,某些情況下又必須知道T的具體類型特征,才能作出特殊的處理。例如對char型的數組元素累加如果最終返回的也是char型的話,很可能越界,因為char隻占8位,范圍很小。我們可以為T的trait創建一個模板AccumulationTraits。具體代碼如下:

//accum1.hpp:累加算法模板:實現為函數模板,引入瞭trait。用數組首部指針及尾部後面的一個指針作參數  
#ifndef ACCUM_HPP  
#define ACCUM_HPP  
#include "accumtraits.hpp"  
#include <iostream>  
template<typename T>  
inline typename AccumulationTraits<T>::AccT accum(T const* beg,T const* end){  
    //返回值類型是要操作的元素類型T的trait  
    typedef typename AccumulationTraits<T>::AccT AccT;  
    AccT total=AccumulationTraits<T>::zero(); //返回具體類型的缺省值  
    while(beg!=end){  //作累加運算  
        total+=*beg;  
        ++beg;  
    }  
    return total; //返回累加的值  
}  
#endif  
//accumtraits.hpp:累加算法模板的trait  
#ifndef ACCUMTRAITS_HPP  
#define ACCUMTRAITS_HPP  
template<typename T>  
class AccumulationTraits; //隻有聲明  
//各個特化的定義  
template<>  
class AccumulationTraits<char>{ //把具體類型char映射到int,累加後就返回int  
public:  
    typedef int AccT;  //統一的類型別名,表示返回類型  
    static AccT zero(){ //關聯一個缺省值,是累加時的初始缺省值  
        return 0;  
    }  
};  
template<>  
class AccumulationTraits<short>{ //把具體類型short映射到累加後的返回類型int  
public:  
    typedef int AccT;  
    static AccT zero(){ //沒有直接在類內部定義static變量並提供缺省值,而是使用瞭函數  
                        //因為類內部隻能對整型和枚舉類型的static變量進行初始化  
                        //其他類型的必須類內部聲明,在外部進行初始化  
        return 0;  
    }  
};  
template<>  
class AccumulationTraits<int>{  
public:  
    typedef long AccT;  
    static AccT zero(){  
        return 0;  
    }  
};  
template<>  
class AccumulationTraits<unsigned int>{  
public:  
    typedef unsigned long AccT;  
    static AccT zero(){  
        return 0;  
    }  
};  
template<>  
class AccumulationTraits<float>{  
public:  
    typedef double AccT;  
    static AccT zero(){  
        return 0;  
    }  
};  
//...  
#endif  
//accum1test.cpp:使用累加算法的客戶端代碼  
#include "accum1.hpp"  
#include <iostream>  
int main(){  
    int num[]={1,2,3,4,5}; //整型數組  
    std::cout<<"the average value of the integer values is "  
        <<accum(&num[0],&num[5])/5<<'/n';  //輸出平均值  
    char name[]="templates"; //創建字符值數組  
    int length=sizeof(name)-1;  
    //輸出平均的字符值,返回的是int型,不會越界  
    std::cout<<"the average value of the characters in /""  
        <<name<<"/" is "<<accum(&name[0],&name[length])/length<<'/n';   
    return 0;  
}  

註意trait模板本身隻是一個聲明,並不提供定義,因為它並不知道參數T具體是什麼類型。trait的定義由針對各個具體類型的特化來提供。trait依賴於原來模板的主參數T,因為它表示的是T的特征信息。這裡使用函數zero()為每個具體類型還關聯瞭一個缺省值,用來作為累加的初始值。為什麼不直接關聯為靜態變量呢?比如static AccT const zero=0。這主要是因為在類內部隻能對整型和枚舉類型的static變量進行初始化,其他類型的必須在類內部聲明,在外部進行初始化。這裡對char型數組元素進行累加時,返回int型,這樣就避免瞭會產生越界的情況。

總結出trait模板技術的核心思想:把模板參數T的具體特征信息抽象成一個獨立的模板,通過特化為具體的類型並為該類型關聯統一的別名,我們就可以在模板中引用這個別名,以獲得T的具體特征信息。註意一個模板參數可能有多種特征,每一個trait都可以抽象成一個trait模板。可見這裡特化是獲得具體差異行為的關鍵。由於在模板中類型T是抽象的,不能獲得它的具體特征,我們通過對T的特征進行抽離,並特化為具體的類型,才能獲得類型的具體特征。從這可以看出我們還有一種實現方案,那就是直接特化模板accum,即針對char型進行一個特化來進行累加。但這樣特化版本中又要重寫基本模板中那些相同的代碼邏輯(比如進行累加的while循環),而實際上我們需要特化的隻是類型的特征信息。

在設計層面上,特化與模板的意圖正好相反。模板是泛型代碼,代表各個類型之間的共性,而特化則表示各個類型之間的差異。我們可以結合多態來深刻地把握這些設計思想。從一般意義上講,polymorphism是指具有多種形態或行為,它能夠根據單一的標記來關聯不同的特定行為。可見條件語句if/else也可以看作是一種多態,它根據標記的不同狀態值來選擇執行不同的分支代碼(代表不同的行為)。多態在不同的程序設計范型有不同的表現。

(1)面向過程的程序設計:多態通過條件語句if/else來實現。這樣多態其實成瞭最基本的程序邏輯結構。我們知道順序語句和條件語句是最基本的邏輯結構,switch語句本身就是if/else的變體,循環語句相當於有一個goto語句的if/else。這種多態可以稱為OP多態,它最大優點就是效率高,隻有一個跳轉語句,不需要額外的開銷。最大缺點就難以擴展,很難應對變化。當有新的行為時,就要修改原來的代碼,在if/else中再增加一個分支,然後重新編譯代碼。它隻是一種低層次的多態,需要程序員人工增加代碼,判斷標記的值。

(2)面向對象程序設計:多態通過虛函數機制,用繼承的方式來實現。這裡的設計思想就是抽離類型之間的共性,把它們放在基類中,而具體的差異性則放到子類中。我們使用基類指針或引用作為單一的標記,它會自動的綁定到子類對象上,以獲得不同的行為。函數重載也可以看作是一種多態,函數名作為單一的標記,我們通過不同的參數類型來調用不同的重載版本,從而獲得不同的多態行為。這種多態稱為OO多態,它的優點就是自動化,易擴展,提高瞭復用程度。它不需要程序員人工幹預,因為動態綁定是自動進行的。當需要新的行為時,從基類繼承一個新的子類即可,不需要修改原來的代碼,系統易維護,也易擴展。缺點就是降低瞭效率,當縱向的繼承體系比較深時,要創建大量的對象,虛函數一般也很少能夠被內聯,這會使內存使用量大幅增加。OO多態是一種高層次的多態,耦合性比OP多態低,但縱向的繼承體系仍然有一定的耦合性。

(3)泛型程序設計:多態通過模板來實現。這裡的設計思想就是不需要抽離類型之間的共性,而是直接對類型進行參數化,把它設計成模板,以表示共性。類型之間的差異通過特化來實現。編譯器會根據類型參數(相當於單一的標記)自動決定是從模板產生實例,還是調用特化的實例。這種多態稱為GP多態,它是橫向的,代表共性的模板與代表差異性的特化在同一層次上,它們之間是相互獨立的,因此它的耦合性更低,性能也更好。由於GP本身也支持繼承和重載,因此可以看出它是一種更高層次的多態,而用模板來做設計甚至比面向對象設計還強大,因為模板本身也支持面向對象的繼承機制,它在面向對象層次上還作瞭一層更高的抽象(對類進行抽象)。GP多態還具有更好的健壯性,因為它在編譯期就進行檢查。當然,GP代碼比較難調試,這主要由於 編譯器支持得不好。

用模板參數來傳遞多種trait

前面我們在accum中通過組合的方式使用它的trait模板。我們也可直接給accum模板增加一個模板參數用來傳遞trait類型,並指定一個缺省實參為AccumulationTraits<T>,這樣可以適應有多種trait的情況。由於函數模板並不能指定缺省模板實參(其實現在許多編譯器都支持這個非標準特性),我們把accum實現為一個類模板。算法作為一個函數來使用時應該會更自然一點,因此可以再用一個函數模板來包裝這個類模板,使之變成一個函數模板。如下:

//accum2.hpp:累加算法模板:實現為類模板,用模板參數來傳遞trait  
//可用一個內聯函數模板作為包裝器來包裝這個類模板實現  
#ifndef ACCUM_HPP  
#define ACCUM_HPP  
#include "accumtraits.hpp"  
template<typename T,typename AT=AccumulationTraits<T> >  
class Accum{ //實現為類模板,模板參數AT代表要使用的trait,並有一個缺省實參  
public:  
    static typename AT::AccT accum(T const* beg,T const* end){  
        typename AT::AccT total=AT::zero(); //獲取缺省值  
        while(beg != end){ //進行累加  
            total+=*beg;  
            ++beg;  
        }  
        return total; //返回累加的值  
    }  
};  
//用內聯的函數模板來包裝,對默認的trait,使用一個獨立的重載版本  
template<typename T>  
inline typename AccumulationTraits<T>::AccT accum(T const* beg,T const* end){  
    return Accum<T>::accum(beg,end);  
}  
template<typename T,typename Traits>  
inline   
typename Traits::AccT accum(T const* beg,T const* end){  
    return Accum<T,Traits>::accum(beg,end);  
}  
#endif  

使模板參數來傳遞trait的一個最大好處是當有多種trait時,我們可以為第2個模板參數指定需要的各種trait。這裡還使用瞭所謂的內聯包裝函數技術。當我們實現瞭一個函數(模板),但接口比較難用時,比如這裡是類模板,用戶即使是使用默認的AccumumationTrait<T>,也要顯式指定第一個實參T,不好用。我們可以用一個包裝函數來包裝它,使其接口變得對用戶非常簡單友好,為瞭避免包裝帶來的性能損失,要把包裝函數(模板)聲明為內聯,編譯器通常會直接調用位於內聯函數裡面的那個函數。這樣,使用默認trait時客戶端代碼accum1test.cpp不需要做任何修改。

policy模板技術

與trait模板技術的思想類似,隻不過是對模板代碼中的算法策略進行抽離。因為模板代碼中對不同的具體類型可能某一部分代碼邏輯(即算法策略)會不一樣(比如對int是累加,對char則是連接)。policy模板就代表瞭這些算法策略。它不需要使用特化,policy隻需重新實現這個與原模板中的代碼不同的具體算法策略即可。

上面是對類型的不同trait產生的差異。實際上對不同的trait,其算法策略(policy)也可能有不同的差異。比如我們對char型元素的數組,不用累加策略,而是用連接的策略。我們還可以把accum看作是一般的數組元素累積性函數,既可以累加,也可以累乘、連接等。一種方法是我們可以直接對accum函數模板的不同具體類型提供特化,重寫各自的代碼邏輯。但實際上,這時我們需要變化的隻有total+=*beg那一條語句,因此我們可以使用policy模板技術,為模板的不同policy創建獨立的模板。這裡我們把policy實現為具有一個成員函數模板的普通類(當然policy也可以直接實現為模板)。對累加策略為SumPolicy,對累乘策略為MultPolicy等。代碼如下:

//policies1.hpp:累加元素模板的不同policy實現:實現為含有成員函數模板的普通類  
#ifndef POLICIES_HPP  
#define POLICIES_HPP  
class SumPolicy{ //累加的policy  
public:  
    template<typename T1,typename T2>  
    static void accumulate(T1& total,T2 const& value){  
        total+=value; //作累加  
    }  
};  
class MultPolicy{ //累乘的policy  
public:  
    template<typename T1,typename T2>  
    static void accumulate(T1& total,T2 const& value){  
        total*=value;  
    }  
};  
//其他各種policy  
//......  
#endif  

引入瞭policy後,把累加算法實現為類模板,如下:

//accum3.hpp:累加算法模板,引入瞭作為普通類的policy,默認是采用SumPolicy  
#ifndef ACCUM_HPP  
#define ACCUM_HPP  
#include "accumtraits.hpp"  
#include "policies1.hpp"  
template<typename T,typename Policy=SumPolicy,typename Traits=AccumulationTraits<T> >  
class Accum{ //累加算法實現為類模板,默認采用SumPolicy  
public:  
    typedef typename Traits::AccT AccT;  
    static AccT accum(T const* beg,T const* end){  
        AccT total=Traits::zero();  //獲取缺省值  
        while(beg !=end){ //作累積運算  
            Policy::accumulate(total,*beg); //使用給定的算法策略來進行累積  
            ++beg;  
        }  
        return total; //返回累積起來的值  
    }  
};  
#endif  

當policy為普通類時,這裡用一個類型模板參數來傳遞不同的policy,缺省的policy為SumPolicy。客戶端使用Accum<int>::accum(&num[0],&num[5])這樣的形式來對int型數組元素進行累加。註意當trait使用默認的AccummulationTrait<T>時,累乘策略MultPolicy實際上就不能用在這裡瞭。因為初始值為0,那累乘的結果最終總是0,可見policy與trait是有聯系的。當然我們也可以換一種方法來實現,即直接讓accum函數增加一個形參T val,用val來指定運算的初始值。實際上,C++標準庫函數accumulate()就是把這個初值作為第3個實參。

模板化的policy

上面的policy實現為具有一個成員函數模板的普通類,這可以看出,其實policy可以直接實現為一個模板。這時在accum算法中就要用模板模板參數來傳遞policy瞭。代碼如下:

//policies2.hpp:把各個policy實現為類模板  
#ifndef POLICIES_HPP  
#define POLICIES_HPP  
template<typename T1,typename T2>  
class SumPolicy{  
public:  
    static void accumulate(T1& total,T2 const& value){  
        total+=value;  
    }  
};  
//...  
#endif  
//accum4.hpp:累加算法模板,引入瞭作為類模板的policy,默認是采用SumPolicy  
#ifndef ACCUM_HPP  
#define ACCUM_HPP  
#include "accumtraits.hpp"  
#include "policies2.hpp"  
template<typename T,  
    template<typename,typename> class Policy=SumPolicy,  
    typename Traits=AccumulationTraits<T> >  
class Accum{ //累加算法實現為類模板,默認采用模板SumPolicy  
public:  
    typedef typename Traits::AccT AccT; //獲取返回類型,它是T的trait  
    static AccT accum(T const* beg,T const* end){  
        AccT total=Traits::zero();  //獲取缺省值  
        while(beg !=end){ //作累積運算  
            Policy<AccT,T>::accumulate(total,*beg); //使用給定的算法策略來進行累積  
            ++beg;  
        }  
        return total; //返回累積起來的值  
    }  
};  
#endif  

trait模板與policy模板技術的比較

(1)trait註重於類型,policy更註重於行為。

(2)trait可以不通過模板參數來傳遞,它表示的類型通常具有自然的缺省值(如int型為0),它依賴於一個或多個主參數,它 一般用模板來實現。

(3)policy可以用普通類來實現,也可以用類模板來實現,一般通過模板參數來傳遞。它並不需要類型有缺省值,缺省值通常是在policy中的成員函數中用一個獨立的參數來傳遞。它通常並不直接依賴於模板參數。

一般在模板中指定兩個模板參數來傳遞trait和policy。而policy的種類更多,使用更頻繁,因此通常代表policy的模板參數在代表trait的模板參數前面。

標準庫中的std::iterator_traits<T>是一個trait,可通過iterator_traits<T>::value_ type來引用T表示的具體類型。其實現也是用特化來獲取各個具體的類型,有全局特化也有局部物化,如指針類型,引用類型等就隻能通過局部特化為T*,T&來實現。

以上就是詳解c++中的trait與policy模板技術的詳細內容,更多關於c++中的trait與policy模板技術的資料請關註WalkonNet其它相關文章!

推薦閱讀: