C++ template —— 模板與繼承(八)


16.1 命名模板參數
許多模板技術往往讓類模板拖着一長串類型參數;不過許多參數都設有合理的缺省值,如:

template <typename policy1 = DefaultPolicy1,
                typename policy2 = DefaultPolicy2,
                typename policy3 = DefaultPolicy3,
                typename policy4 = DefaultPolicy4>
class BreadSlicer
{
    .......
};

一般情況下使用缺省模板實參BreadSlicer<>就足夠了。不過,如果必須指定某個非缺省的實參,還必須明白地指定在它之前的所有實參(即使這些實參正好是缺省類型,也不能偷懶)。 跟這樣的BreadSlicer<DefaultPolicy1, DefaultPolicy2, Custom>相比,BreadSlicer<Policy3 = Custom>顯然更有吸引力。這也是本節要介紹的內容。

我們的考慮主要是設法將缺省類型值放到一個基類中,再根據需要通過派生覆蓋掉某些類型值。這樣,我們就不再直接指定類型實參了,而是通過輔助類完成,如BreadSlicer<Policy3_is<Custom> >。既然用輔助類做模板參數,每個輔助類都可以描述上述4個policy中的任意一個,故所有模板參數的缺省值均相同:

template <typename PolicySetter1 = DefaultPolicyArgs,
                typename PolicySetter2 = DefaultPolicyArgs,
                typename PolicySetter3 = DefaultPolicyArgs,
                typename PolicySetter4 = DefaultPolicyArgs>
class BreadSlicer
{
    typedef PolicySelector<PolicySetter1, PolicySetter2,
                                     PolicySetter3, PolicySetter4>
                Policies;
    // 使用Policies::P1, Policies::P2, ……來引用各個Policies
};


剩下的麻煩事就是實現模板PolicySelector。這個模板的任務是利用typedef將各個模板實參合並到一個單一的類型(即Discriminator),該類型能夠根據指定的非缺省類型(如policy1-is的Policy),改寫缺省定義的typedef成員(如Default Policies的DefaultPolicy1)。其中合並的事情可以讓繼承來干:

// PolicySelector<A, B, C, D>生成A, B, C, D作為基類
// Discriminator<>使Policy Selector可以多次繼承自相同的基類
// PolicySelector不能直接從Setter繼承
template <typename Base, int D>
class Discriminator : public Base{
};

template <typename Setter1, typename Setter2,
                typename Setter3, typename Setter4>
class PolicySelector : public Discriminator<Setter1, 1>,
                                public Discriminator<Setter2, 2>,
                                public Discriminator<Setter3, 3>,
                                public Discriminator<Setter4, 4>{
};

注意,由於中間模板Discriminator的引入,我們就可以一致處理各個Setter類型(不能直接從多個相同類型的基類繼承,但可以借助中間類間接繼承)。
如前所述,我們還需要把缺省值集中到一個基類中:

// 分別命名缺省policies為P1, P2, P3, P4
class DefaultPolicies
{
    public:
        typedef DefaultPolicy1 P1;
        typedef DefaultPolicy2 P2;
        typedef DefaultPolicy3 P3;
        typedef DefaultPolicy4 P4;
};

不過由於會多次從這個基類繼承,我們必須小心以避免二義性,故用虛擬繼承:

// 一個為了使用缺省policy值的類
// 如果我們多次派生自DefaultPolicies,下面的虛擬繼承就避免了二義性
class DefaultPolicyArgs : virtual public DefaultPolicies{
};

最后,我們只需要寫幾個模板覆蓋掉缺省的policy參數:

template <typename Policy>
class Policy1_is : virtual public DefaultPolicies
{
    public:
        typedef Policy P1;           //改寫缺省的typedef
};

template <typename Policy>
class Policy2_is : virtual public DefaultPolicies
{
    public:
        typedef Policy P2;           //改寫缺省的typedef
};

template <typename Policy>
class Policy3_is : virtual public DefaultPolicies
{
    public:
        typedef Policy P3;           //改寫缺省的typedef
};

template <typename Policy>
class Policy4_is : virtual public DefaultPolicies
{
    public:
        typedef Policy P4;           //改寫缺省的typedef
};

最后,我們把模板BreadSlicer實例化為:

BreadSlicer<Policy3_is<CustomPolicy> > bc;

這時模板BreadSlicer中的類型Polices被定義為:

PolicySelector<Policy3_is<CustomPolicy>,
                    DefaultPolicyArgs,
                    DefaultPolicyArgs,
                    DefaultPolicyArgs>

由類模板Discriminator的幫助,我們得到了圖16.1所示的類層次。從中可以看出,所有的模板實參都是基類,而它們有共同的虛基類DefaultPolicies,正是這個共同的虛基類定義了P1, P2, P3和P4的缺省類型;不過,其中一個派生類Policy3_is<>重定義了P3。根據優勢規則,重定義的類型隱藏了基類中的定義,這里沒有二義性問題。

在模板BreadSlicer中,我們可以使用諸如Policies::P3等限定名稱來引用這4個policy,例如:

template <... >
class BreadSlicer
{
    ...
    public:
        void print(){
        Policies::P3::doPrint();
    }
    ...
};

16.2 空基類優化

C++類常常為“空”,這就意味着在運行期其內部表示不耗費任何內存。這常見於只包含類型成員、非虛成員函數和靜態數據成員的類,而非靜態數據成員、虛函數和虛基類則的確在運行期耗費內存。

即使是空類,其大小也不會是0。在某些對於對齊要求更嚴格系統上也會有差異。

16.2.1 布局原則

C++的設計者們不允許類的大小為0,其原因很多。比如由它們構成的數組,其大小必然也是0,這會導致指針運算中普遍使用的性質失效。

雖然不能存在“0大小”的類,但C++標准規定,當空類作為基類時,只要不會與同一類型的另一個對象或子對象分配在同一地址,就不需要為其分配任何空間。我們通過實例來看看這個所謂的空基類優化(empty base class optimization, EBCO)技術:

// inherit/ebco1.cpp
#include <iostream>

class Empty
{
    typedef int Int;       // typedef 成員並不會使類成為非空
};

class EmptyToo : public EmptyToo
{
};

class EmptyThree : public EmptyToo
{
};

int main()
{
    std::cout << "sizeof(Empty) :             " << sizeof(Empty) << '\n';
    std::cout << "sizeof(EmptyToo) :             " << sizeof(EmptyToo) << '\n';
    std::cout << "sizeof(EmptyThree) :             " << sizeof(EmptyThree) << '\n';
}

如果編譯器支持空基類優化,上述程序所有的輸出結果相同,但均不為0(見圖16.2)。也就是說,在類EmptyToo中的類Empty沒有分配空間。注意,帶有優化空基類的空類(沒有其他基類),其大小亦為0;這也是類EmptyThree能夠和類Empty具有相同大小的原因所在。然而,在不支持EBCO的編譯器上,結果就大相徑庭(見圖16.3)。

想想在空基類優化下,下例的結果如何?

// inherit/ebco2.cpp
#include <iostream>
class Empty
{
    typedef int Int;         // typedef 成員並沒有使一個類變成非空
};

class EmptyToo : public Empty
{
};

class NonEmpty : public Empty, public EmptyToo
{
};

int main()
{
    std::cout << "sizeof(Empty) :            " << sizeof(Empty) << '\n';
    std::cout << "sizeof(EmptyToo) :            " << sizeof(EmptyToo) << '\n';
    std::cout << "sizeof(NonEmpty) :            " << sizeof(NonEmpty) << '\n';
}

也許你會大吃一驚,類NonEmpty並非真正的“空”類,但的的確確它和它的基類都沒有任何成員。不過,NonEmpty的基類Empty和EmptyToo不能分配到同一地址空間,否則EmptyToo的基類Empty會和NonEmpty的基類Empty撞在同一地址空間上。換句話說,兩個相同類型的子對象偏移量相同,這是C++對象布局規則不允許的。有人可能會認為可以把兩個Empty子對象分別放在偏移0和1字節處,但整個對象的大小也不能僅為1.因為在一個包含兩個NonEmpty的數組中,第一個元素和第二個元素的Empty子對象也不能撞在同一地址空間(見圖16.4)。

對空基類優化進行限制的根本原因在於,我們需要能比較兩個指針是否指向同一對象,由於指針幾乎總是用地址作內部表示,所以我們必須保證兩個不同的地址(即兩個不同的指針值)對應兩個不同的對象。
雖然這種約束看起來並不非常重要,但是在實際應用中的許多類都是繼承自一組定義公共typedefs的基類,當這些類作為子對象出現在同一對象中時,問題就凸現出來了,此時優化應該被禁止。

16.2.2 成員作基類
書中介紹了將成員作基類的技術。但對於數據成員,則不存在類似空基類優化的技術,否則遇到指向成員的指針時就會出問題。
將成員變量實現為(私有)基類的形式,在模板中考慮這個問題特別有意義,因為模板參數常常可能就是空類(雖然我們不可以依賴這個規則)。

16.3 奇特的遞歸模板模式
奇特的遞歸模板模式(Curiously Recurring Template Pattern, CRTP)這個奇特的名字代表了類實現技術中一種通用的模式,即派生類將本身作為模板參數傳遞給基類。最簡單的情形如下:

template <typename Derived>
class CuriousBase
{
    ....
};

class Curious : public CuriousBase<Curious>         // 普通派生類
{
    ....
};

在第一個實例中,CRTP有一個非依賴型基類:類Curious不是模板,因此免於與依賴型基類的名字可見性等問題糾纏。不過,這並非CRTP的本質特征,請看:

template <typename Derived>
class CuriousBase
{
    ....
};

template <typename T>
class CuriousTemplate : public CuriousBase<CuriousTemplate<T> >       // 派生類也是模板
{
    ...
};

從這個示例出發,不難再舉出使用模板的模板參數的方式:

template <template<typename> class Derived>
class MoreCuriousBase
{
    ....
};

template <typename T>
class MoreCurious : public MoreCuriousBase<MoreCurious>
{
    ....
};

CRTP的一個簡單應用是記錄某個類的對象構造的總個數。數對象個數很簡單,只需要引入一個整數類型的靜態數據成員,分別在構造函數和析構函數中進行遞增和遞減操作。不過,要在每個類里都這么寫就很繁瑣了。有了CRTP,我們可以先寫一個模板:

// inherit/objectcounter.hpp

#include <stddef.h>

template <typename CountedType>
class ObjectCounter
{
    private:
        static size_t cout;    // 存在對象的個數
    
    protected:
        //缺省構造函數
        ObjectCounter(){
            ++ObjectCounter<countedType>::count;
        }
        // 拷貝構造函數
        ObjectCounter(ObjectCounter<countedType> const&){
            ++ObjectCounter<countedType>::count;
        }
        // 析構函數
        ~ObjectCounter(){
            --ObjectCounter<countedType>::count;
        }
    
    public:
        // 返回存在對象的個數:
        static size_t live(){
            return ObjectCounter<countedType>::count;
        }
};

// 用0來初始化count
template <typename CountedType>
size_t ObjectCounter<CountedType>::count = 0;

如果想要數某個類的對象存在的個數,只需讓該類從模板ObjectCounter派生即可。以一個字符串類為例:

//inherit/testcounter.cpp

#include "objectcounter.hpp"
#include <iostream>

template <typename CharT>
class MyString : public ObjectCounter<MyString<CharT> >
{
    ....
};

int main()
{
    MyString<char> s1, s2;
    MyString<wchar_t> ws;

    std::cout << "number of MyString<char> : " << MyString<char>::live() << std::endl;
    std::cout << "number of MyString<wchar_t> : " << MyString<wchar_t>::live() << std::endl;
}

一般地,CRTP適用於僅能用作成員函數的接口(如構造函數、析構函數和小標運算operator[]等)的實現提取出來。

16.4 參數化虛擬性

C++允許通過模板直接參數化3種實體:類型、常數(nontype)和模板。同時,模板還能間接參數化其他屬性,比如成員函數的虛擬性。

// inherit/virtual.cpp

#include <iostream>
class NotVirtual
{
};

class Virtual
{
    public:
        virtual void foo(){
    }
};

template <typename VBase>
class Base : private VBase
{
    public:
        // foo()的虛擬性依賴於它在基類VBase(如果存在基類的話)中聲明
        void foo(){
            std::cout << "Base::foo() " << '\n';
        }
};

template <typename V>
class Derived : public Base<V>
{
    public:
        void foo(){
        std::cout << "Derived::foo() " << '\n';
    }
};

int main()
{
    Base<NotVirtual>* p1 = new Derived<NotVirtual>;
    p1->foo();      // 調用Base::foo()

    Base<Virtual>* p2 = new Derived<Virtual>;
    p2->foo();       // 調用Derived::foo()
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM