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() }