More Effective C++: 05技術(25-28)
25:將constructor 和 non-member functions 虛化
所謂 virtual constructor是某種函數,視其輸入可產生不同類型的對象。比如下面的代碼:
class NLComponent { public: ... }; class TextBlock: public NLComponent { public: ... }; class Graphic: public NLComponent { public: ... }; class NewsLetter { public: NLComponent*readComponent(std::string &str); ... private: list<NLComponent*> components; };
readComponent根據參數str,決定產生TextBlock或Graphic。由於它產生新的對象,所以行為仿若constructor,但由於它能夠產生不同型別的對象,所以稱它為一個virtual constructor。
有一種特別的virtual constructor,稱為virtual copy constructor,它會傳回一個指針,指向其調用者(某對象)的一個新副本。比如下面的clone函數:
class NLComponent { public: virtual NLComponent * clone() const = 0; ... }; class TextBlock: public NLComponent { public: virtual TextBlock * clone() const { return new TextBlock(*this); } ... }; class Graphic: public NLComponent { public: virtual Graphic * clone() const {return new Graphic(*this); } ... };
註意,當 derived class 重新定義其base class 的一個虛函數時,如果函數的返回類型是個指針或引用),指向一個base class,那麽derived class的函數可以返回一個指針或引用,指向該base class的一個derived class。
所謂將non-member functions 虛化,也就是讓non-member functions的行為視其參數的動態類型而不同。比如下面的代碼:
class NLComponent { public: virtual ostream& print(ostream& s) const = 0; ... }; class TextBlock: public NLComponent { public: virtual ostream& print(ostream& s) const; ... }; class Graphic: public NLComponent { public: virtual ostream& print(ostream& s) const; ... }; inline ostream& operator<<(ostream& s, const NLComponent& c) { return c.print(s); }
上面的operator<<就相當於一個虛化的non-member函數。註意,不能將operator<<定義為成員函數,這是因為operator<<的第一個參數必須是ostream變量。
26:限制某個class所能產生的對象數量
1;允許0或1個對象
每當即將產生一個對象,我們確知一件事情:會有一個構造函數被調用。因此,阻止某個class產出對象的最簡單方法就是將其構造函數聲明為private。
如果想要限制只能產生一個對象,則可以這樣:
class PrintJob; class Printer { public: void submitJob(const PrintJob& job); void reset(); void performSelfTest(); ... friend Printer& thePrinter(); private: Printer(); Printer(const Printer& rhs); ... }; Printer& thePrinter() { static Printer p; //唯一的一個打印機對象 return p; } string buffer; thePrinter().reset(); thePrinter().submitJob(buffer);
這樣的設計有3個成分:一,Printer class的constructors為private,可以抑制對象的產生;二,全局函數thePrinter被聲明為此class的一個friend,致使thePrinter不受private constructors 的約束;三,thePrinter內含一個 static Printer對象,意思是只有一個Printer對象會被產生出來。
也可以將thePrinter聲明為Printer的一個static member函數:
class Printer { public: static Printer& thePrinter(); ... private: Printer(); Printer(const Printer& rhs); ... }; Printer& Printer::thePrinter() { static Printer p; return p; }
以上兩個thePrinter函數的實現,有個精細的地方值得討論:形成唯一一個Printer對象的,是函數中的static對象,而非class中static對象。這點很重要,class擁有一個static對象的意思是,縱使從未被用到,它也會被建構(及解構)。而函數中的static對象,只有在函數第一次被調用時才會產生。另外,函數內的static對象的初始化時機是確定的(第一次調用),而class的static對象則不一定在什麽時候初始化。C++對於同一編譯單元內的static對象的初始化次序是有提出一些保證的,但對於不同編譯單元內的static對象的初始化次序沒有任何說明。
或許你認為更好的作法是簡單地計算目前存在的對象個數,當外界創建太多對象時,在 構造函數內丟出一個異常。比如下面的代碼:
class Printer { public: class TooManyObjects{}; // 當創建的對象過多時就使用這個異常類 Printer(); ~Printer(); ... private: static size_t numObjects; Printer(const Printer& rhs); // 這裏只能有一個printer,所以不允許拷貝 }; size_t Printer::numObjects = 0; Printer::Printer() { if (numObjects >= 1) { throw TooManyObjects(); } 繼續運行正常的構造函數; ++numObjects; } Printer::~Printer() { 進行正常的析構函數處理; --numObjects; }
這種限制對象數量的方法更直接易懂,而且很容易被一般化,使對象的最大數量可以設定為1以外的值。
但是這種策略也有問題。當Printer被繼承,或是被包含於其他類內部時,繼承類或包含類的數目也被限制住了:
class ColorPrinter: public Printer { ... }; Printer p; ColorPrinter cp;
這裏實際上創建了兩個Printer對象,一個是 p,另一個是cp內的“Printer 成份”。一旦執行,在cp的“base class部分”建構時,會有一個TooManyObjects異常被拋出。
class CPFMachine { private: Printer p; ... }; CPFMachine m1; CPFMachine m2;
當創建m2時,也會拋出一個TooManyObjects異常。
問題原因在於:Printer對象以三種不同的狀態而存在:它自己;繼承類的“base class部分”;內嵌於其他對象之中。這些不同狀態的呈現,把“追蹤目前存在的對象個數”的意義嚴重弄混了。你心裏頭所想的“目前存在的對象個數”可能和編譯器所想的不同。
如果采用原先的策略的話,因為Printer構造函數是private,且如果沒有聲明任何friend的話,則帶有private constructors的類一旦被繼承,或被內嵌於其他類內,則繼承類或包含類不能在創建對象。
還有一點值得註意:使用thePrinter函數封裝對單個對象的訪問,以便把Printer對象的數量限制為一個,這樣做的同時也會使每一次運行程序時只能使用一個Printer對象。導致我們不能這樣編寫代碼:
建立 Printer 對象 p1;
使用 p1;
釋放 p1;
建立Printer對象p2;
使用 p2;
釋放 p2;
這種設計在同一時間裏沒有實例化多個Printer對象,而是在程序的不同時間使用了不同的Printer對象。不允許這樣編寫有些不合理。畢竟我們沒有違反只能存在一個printer的約束。下面的使這種方式成為可能
當然有。可以把先前使用的對象計數的代碼與剛才看到的偽構造函數代碼合並在一起:
class Printer { public: class TooManyObjects{}; static Printer * makePrinter(); // 偽構造函數 ~Printer(); void submitJob(const PrintJob& job); void reset(); void performSelfTest(); ... private: static size_t numObjects; Printer(); Printer(const Printer& rhs); }; size_t Printer::numObjects = 0; Printer::Printer() { if (numObjects >= 1) { throw TooManyObjects(); } 繼續運行正常的構造函數; ++numObjects; } Printer * Printer::makePrinter() { return new Printer; }
除了用戶必須調用偽構造函數,而不是真正的構造函數之外,它們使用Printer類就像使用其他類一樣:
Printer p1; // 錯誤! 缺省構造函數是private Printer *p2 = Printer::makePrinter(); // 正確, 間接調用缺省構造函數 Printer p3 = *p2; // 錯誤! 拷貝構造函數是private p2->performSelfTest(); // 所有其它的函數都可以正常調用 p2->reset(); ... delete p2;
這種技術很容易推廣到限制對象為任何數量上。只需把numObjects常量值1改為其他值,然後消除拷貝對象的約束即可:
class Printer { public: class TooManyObjects{}; static Printer * makePrinter(); // 偽構造函數 static Printer * makePrinter(const Printer& rhs); //偽復制構造函數 ... private: static size_t numObjects; static const size_t maxObjects = 10; Printer(); Printer(const Printer& rhs); }; size_t Printer::numObjects = 0; Printer::Printer() { if (numObjects >= maxObjects) { throw TooManyObjects(); } ... } Printer::Printer(const Printer& rhs) { if (numObjects >= maxObjects) { throw TooManyObjects(); } ... } Printer * Printer::makePrinter() { return new Printer; } Printer * Printer::makePrinter(const Printer& rhs) { return new Printer(rhs); }
2:一個具有對象計數功能的基類
如果我們有大量像Printer這樣需要限制對象數量的類,此時就需要編寫一個具有對象計數功能的基類,然後讓像Printer這樣的類從該基類繼承。
在基類中封裝全部的計數功能,包括靜態變量numObjects,為了確保每個進行實例計數的類都有一個相互隔離的計數器,可以使用計數類模板:
template<class BeingCounted> class Counted { public: class TooManyObjects{}; // 用來拋出異常 static int objectCount() { return numObjects; } protected: Counted(); Counted(const Counted& rhs); ~Counted() { --numObjects; } private: static int numObjects; static const size_t maxObjects; void init(); }; template<class BeingCounted> Counted<BeingCounted>::Counted() { init(); } template<class BeingCounted> Counted<BeingCounted>::Counted(const Counted<BeingCounted>&) { init(); } template<class BeingCounted> void Counted<BeingCounted>::init() { if (numObjects >= maxObjects) throw TooManyObjects(); ++numObjects; }
這個模板生成的類僅能被做為基類使用,因此構造函數和析構函數被聲明為protected。現在我們能修改Printer類,這樣使用Counted模板:
class Printer: private Counted<Printer> { public: static Printer * makePrinter();// 偽構造函數 static Printer * makePrinter(const Printer& rhs); ~Printer(); void submitJob(const PrintJob& job); void reset(); void performSelfTest(); ... using Counted<Printer>::objectCount; // 參見下面解釋 using Counted<Printer>::TooManyObjects; // 參見下面解釋 private: Printer(); Printer(const Printer& rhs); bbs.theithome.com }; Printer::Printer() { 進行正常的構造函數運行 }
Printer類private繼承Counter模板,因為它們之間是implemented-in-terms-of的關系。
Counted所做的大部分工作對於Printer的用戶來說都是隱藏的,但是這些用戶可能很想知道有當前多少Printer對象存在。Counted模板提供了objectCount函數,用來提供這種信息,但是因為我們使用private繼承,這個函數在Printer類中成為了private。為了恢復該函數的public訪問權,我們使用using聲明。TooManyObjects類也用同樣的方式來處理,因為Printer的客戶端如果要捕獲這種異常類型,它們必須有能力訪問TooManyObjects。
當Printer繼承Counted<Printer>時,它可以忘記有關對象計數的事情。編寫Printer類時根本不用考慮對象計數。
最後還有一點需要註意,必須定義Counted內的靜態成員。對於numObjects來說,只需要在Counted的實現文件裏定義它即可:
template<class BeingCounted> // 定義numObjects int Counted<BeingCounted>::numObjects; // 自動把它初始化為0
對於maxObjects來說,則有一些技巧。有的類可能需要限制對象數量為10,有的則為16。這種情況下,我們不對maxObject進行初始化。而是讓此類的客戶端提供合適的初始化。比如Printer的作者必須把這條語句加入到一個實現文件裏:
template<> const size_t Counted<Printer>::maxObjects = 2;
如果這些作者忘了對maxObjects進行初始化,連接時會發生錯誤:” undefined reference to `Counted<Printer>::maxObjects‘”。
27:要求(或禁止)對象產生於heap之中
1:要求對象產生於heap之中
為了限制對象必須產生於heap之中,需要阻止用戶不得使用new以外的方法產生對象。non-heap objects會在其定義點自動建構,並在其壽命結束時自動解構,所以只要讓那些被隱秘調用的構造動作和析構動作不合法就可以了。最直接的方式就是將構造函數和析構函數都聲明為private,但這實在太過了,比較好的辦法是讓析構函數成為private而構造函數仍為public:
class UPNumber { public: UPNumber(); UPNumber(int initValue); UPNumber(double initValue); UPNumber(const UPNumber& rhs); // 偽析構函數。這是一個 const member function, // 因為 const 對象也可能需要被摧毀。 void destroy() const { delete this; } ... private: ~UPNumber(); }; UPNumber n; // 編譯錯誤! UPNumber *p = new UPNumber; // 良好。 ... delete p; // 編譯錯誤! p->destroy(); // 良好。
將析構函數聲明為private之後,”UPNumber n;”和”delete p;”都會報編譯錯誤:‘UPNumber::~UPNumber()’ is private, within this context UPNumber n(或delete p);
但是,就像之前提到過的,這種方法也阻止了繼承和包含:UPNumber無法被繼承,也無法被其他類包含。單純聲明繼承或包含UPNumber的類雖然可以通過編譯,但是卻無法使用,不能產生這種類的對象。這些困難都可以克服。令UPNumber的析構函數成為 protected(並仍保持其構造函數為public),便可解決繼承問題;將“內含UPNumber對象”的類,改為“包含一個指針,指向 UPNumber對象”,可解決包含問題。
2:判斷某個對象是否位於Heap內
將析構函數聲明為protected,可以解決繼承問題,使得”NonNegativeUPNumber n;”這樣的語句能編譯通過。但是,在n中,基類UPNumber的部分實際上在棧中而不是heap中。如果需要限制住所有UPNumber對象(包括繼承類對象中的UPNumber部分)都必須在堆中,怎麽辦?
實際上沒有簡單有效的辦法,因為UPNumber構造函數不可能知道它被調用是否是為了產生某個heap中繼承類對象的基類部分。或許你認為下面使用operator new的方法能夠解決這個問題:
class UPNumber { public: class HeapConstraintViolation {}; //如果產生一個非heap對象,就丟出該異常 static void * operator new(size_t size); UPNumber(); ... private: static bool onTheHeap; ... }; bool UPNumber::onTheHeap = false; void *UPNumber::operator new(size_t size) { onTheHeap = true; return ::operator new(size); } UPNumber::UPNumber() { if (!onTheHeap) { throw HeapConstraintViolation(); } proceed with normal construction here; onTheHeap = false; // 清除 flag,供下一個對象使用。 }
operator new將onTheHeap置為true,而每一個構造函數都會檢查onTheHeap的值,判斷建構中的對象的內存是否由operator new 配置而來。如果不是,就丟出一個異常,否則構造函數就如常繼續下去。一旦建構完成,onTheHeap會被設為false,為下一個將被建構的對象重新設定好默認值。 這種方法有幾個問題:
像” UPNumber *numberArray = new UPNumber[100];”這樣的語句,即使在operator new[]中做類似的配置,但是這裏分配內存的動作只有一次,而構造函數卻需要被調用100次,在第二次調用構造函數時,就會拋出一個異常;
像”UPNumber *pn = new UPNumber(*new UPNumber);”這樣的語句(先忽略內存泄漏),編譯器可能產生的動作是:為第一個對象調用operator new,為第二個對象調用operator new,為第一個對象調用構造函數,為第二個對象調用構造函數。這時的問題跟上面的數組的問題類似。
為了能讓構造函數判斷*this是否位於heap中,你或許會采用一種“不可移植”的方法。許多操作系統中,程序的地址空間,stack地址空間在高地址處,地址從高地址往低地址成長,heap地址空間在低地址處,地址由低地址往高地址成長。你覺得可以利用下面的函數判斷某個地址是否位於heap中:
bool onHeap(const void *address){ char onTheStack; return address < &onTheStack; }
在onHeap函數內,onTheStack是個局部變量。它被置於stack內。當onHeap被調用,其stack frame會被放在stack的最頂端,由於此架構中的stack向低地址成長,所以onTheStack的地址一定比其他任何一個位於stack 中的變量(或對象)更低。因此,如果參數address比onTheStack的地址更低,它就不可能位於stack,那就一定是位於heap。
這種方法的問題在於,除了棧和堆,還有靜態對象的地址空間,它通常位於堆之下,因此,上面的函數無法區分堆對象和靜態對象。因此,為了判斷某個地址是否位於heap中,一定會走入不可移植的,視系統而異的陰暗角落。所以你最好重新設計你的軟件,避免需要判斷某個對象是否位於heap內。
判斷地址是否存在於heap中是困難的,但是判斷某個地址被delete是否安全卻相對簡單些。只要地址是new出來的,delete它就是安全的。可以設計一個abstract mixin base class,用於為派生類提供“判斷某地址是否是operator new出來的”的能力:
class HeapTracked { public: class MissingAddress{}; virtual ~HeapTracked() = 0; static void *operator new(size_t size); static void operator delete(void *ptr); bool isOnHeap() const; private: typedef const void* RawAddress; static list<RawAddress> addresses; }; list<RawAddress> HeapTracked::addresses; HeapTracked::~HeapTracked() {} void * HeapTracked::operator new(size_t size) { void *memPtr = ::operator new(size); addresses.push_front(memPtr); return memPtr; } void HeapTracked::operator delete(void *ptr) { list<RawAddress>::iterator it = find(addresses.begin(), addresses.end(), ptr); if (it != addresses.end()) { addresses.erase(it); ::operator delete(ptr); } else { //表示ptr不是operator new所返回 throw MissingAddress(); } } bool HeapTracked::isOnHeap() const { const void *rawAddress = dynamic_cast<const void*>(this); list<RawAddress>::iterator it = find(addresses.begin(), addresses.end(), rawAddress); return it != addresses.end(); }
需要註意的是下面的語句:const void *rawAddress = dynamic_cast<const void*>(this);
這是因為凡涉及“多重或虛擬基類”的對象,會擁有多個地址,因為isOnHeap只施行於HeapTracked對象身上,我們可以利用dynamic_cast的特殊性質來消除這個問題。只要簡單地將指針“動態轉型”為void*(或const void* 或 volatile void* 或 const volatile void*),便會獲得一個指針,指向“普通指針所指對象”的內存起始處。不過,dynamic_cast只適用於“所指對象至少有一個虛函數”的指針身上,這對於HeapTracked及其子類而言不成問題。
定義了該抽象基類之後,就可以為任何類加上“追蹤指針”的能力:
class Asset: public HeapTracked { private: UPNumber value; ... }; void inventoryAsset(const Asset *ap) { if (ap->isOnHeap()) { ap is a heap-based asset — inventory it as such; } else { ap is a non-heap-based asset — record it that way; } }
3:禁止對象產生於Heap內
如前所述,對象的存在有三種可能:直接被構造;被構造為派生類對象中的基類部分;內嵌於其他類對象之中。
為了防止對象被直接構造在heap中,可以在類中聲明operator new為private:
class UPNumber { private: static void *operator new(size_t size); static void operator delete(void *ptr); ... }; UPNumber n1; // 可以 static UPNumber n2; // 可以 UPNumber *p = new UPNumber; // 錯誤!企圖調用private operator new
如果也想禁止“由UPNumber對象所組成的數組”位於heap內,可以將operator new[] 和operator delete[]也聲明為private。
將operator new聲明為private,往往也會阻止UPNumber對象被構建為heap-based 派生類對象的基類部分。那是因為operator new和operator delete都會被繼承,所以如果這些函數不在派生類內聲明為public,派生類便繼承其基類中的版本:
class NonNegativeUPNumber:public UPNumber { ... }; NonNegativeUPNumber n1; //可以 static NonNegativeUPNumber n2; //可以 NonNegativeUPNumber *p = new NonNegativeUPNumber; //錯誤
但是如果派生類聲明了一個屬於自己的operator new,且為public,則該派生類可被構建與heap中。因此需要另覓良法以求阻止“UPNumber作為基類部分”被構建。類似的,當企圖構建一個“包含UPNumber對象”的對象時,“UPNumber的operator new為private”這一事實並不會帶來什麽影響:
class Asset { public: Asset(int initValue); ... private: UPNumber value; }; Asset *pa = new Asset(100); //沒問題,調用的是Asset::operator new //或::operator new,而非UPNumber::operator new
這就又回到了之前的問題:沒有任何可移植的作法用以判斷某地址是否位於heap內一樣,我們也沒有可移植性的方法可以判斷它是否不在heap 內。
28:智能指針
智能指針的operator*的偽代碼如下:
template<class T> T& SmartPtr<T>::operator*() const { perform "smart pointer" processing; return *pointee; }
註意,返回的是引用。如果返回的是對象,結果會慘不忍睹。首先,pointee不一定指向類型為T的對象,也有可能指向T派生類的對象,若真如此返回對象就會發生截斷;而且也不能支持像*p=3這樣的操作。
為了使智能指針能像普通指針那樣,方便的判斷它是否為null:
SmartPtr<TreeNode> ptn; if (ptn == 0) ... if (ptn) ... if (!ptn) ...
需要提供一種隱式類型轉換,傳統做法是轉換為void*:
template<class T> class SmartPtr { public: ... operator void*();//如果smart ptr是null,傳回零,否則傳回非零值。 };
但是就像所有隱式類型轉換一樣,此處這個也有相同的缺點:在程序員認為“函式調用失敗”的情況下,編譯器卻讓它成功:
SmartPtr<Apple> pa; SmartPtr<Orange> po; if (pa == po) ... //竟然可以過關
以“轉換至void*”為基調,可衍生出各種變形。某些設計者喜歡轉換為const void*,另一些人喜歡轉換為 bool,但沒有一個可以消除上述問題。
除非萬不得已,不要提供對普通指針的隱式轉換運算符。智能指針如果提供隱式轉換至 普通指針,便是打開了一個難纏BUG的門戶。比如:
DBPtr<Tuple> pt = new Tuple; ... delete pt;
這應該無法編譯,畢竟 pt是對象而非指針,delete只能操作指針。編譯器會尋找隱式類型轉換,盡可能讓函數調用成功。delete會調用析構函數和operator delete。所以在上述的delete語句中,它暗自將pt 轉換為一個Tuple*,然後刪除。這幾乎一定會弄糟你的程序。該對象現在被刪除了兩次,一次是在delete調用時,一次是在pt 的析構函數被調用時(離開作用於自動析構)。將對象刪除一次以上會導至未定義的行為。
智能指針之間的轉換,可以通過成員函數模板(member function template)來實現,通過這種技術,如果有個普通指針類型為T1*,另一個普通指針類型為T2*,只要能夠將T1*隱式轉換為T2*,便能夠將smart pointer-to-T1 隱式轉換為 smart pointer-to-T2:
class MusicProduct{}; class Cassette: public MusicProduct{}; template<class T> class SmartPtr { public: SmartPtr(T* realPtr = 0); template<class newType> operator SmartPtr<newType>() { return SmartPtr<newType>(pointee); } ... private: T *pointee; }; void displayAndPlay(const SmartPtr<MusicProduct>& pmp, int howMany); SmartPtr<Cassette> funMusic(new Cassette("Alapalooza")); displayAndPlay(funMusic, 10);
funMusic對象屬於SmartPtr<Cassette> 類型,而displayAndPlay函數期望得到的是個 SmartPtr<MusicProduct>對象。編譯器偵測到類型不吻合,於是尋找某種方法,企圖將 funMusic轉換為一個SmartPtr<MusicProduct>對象:編譯器在SmartPtr<MusicProduct>內企圖尋找一個“單一自變量之constructor”,且其自變量類型為SmartPtr<Cassette>,但是沒有找到;於是再接再勵在SmartPtr<Cassette>內尋找一個隱式類型轉換運算符,希望可以產出一個SmartPtr<MusicProduct>;也失敗了,接下來編譯器再試圖尋找一個“可具現化以導出合適之轉換函數”的member function template。這一次它們在SmartPtr<Cassette> 找到了這樣一個東西,當它被具現化並令newType綁定至MusicProduct時,產生了所需函數。於是編譯器將該函數具現化,導出以下函數碼:
SmartPtr<Cassette>::operator SmartPtr<MusicProduct>() { return SmartPtr<MusicProduct>(pointee); }
這裏的問題是否可以以一個 Cassette*指針建構出一個 SmartPtr<MusicProduct>對象。SmartPtr<MusicProduct>構造函數期望獲得一個MusicProduct*指針,很明顯地Cassette*可被交給一個期望獲得MusicProduct*的函數。因此SmartPtr<MusicProduct> 的構建會成功,而 SmartPtr<Cassette>至SmartPtr<MusicProduct>的轉換也會成功。
這項技術也不是萬能的。假設擴充MusicProduct繼承體系,加上一個新的CasSingle類,使其繼承Cassette。現在考慮這份代碼:
template<class T> class SmartPtr { ... }; void displayAndPlay(const SmartPtr<MusicProduct>& pmp, int howMany); void displayAndPlay(const SmartPtr<Cassette>& pc, int howMany); SmartPtr<CasSingle> dumbMusic(new CasSingle("Achy Breaky Heart")); displayAndPlay(dumbMusic, 1); //編譯錯誤!
displayAndPlay被重載,一個接收SmartPtr<MusicProduct>對象,另一個接收SmartPtr<Cassette>對象。當我們調用displayAndPlay並給予一個SmartPtr<CasSingle>,我們預期調用的是SmartPtr<Cassette>函數,因為CasSingle直接繼承自Cassette而間接繼承自MusicProduct。如果面對的是普通指針,情況確實是這樣的。
但只能指針沒有那麽機靈,它們把member functions拿來做為轉換運算符使用,而C++的理念是,對任何轉換函數的調用動作,都是等同視之。於是,displayAndPlay的調用動作成為一種有二義性的行為,因為從SmartPtr<CasSingle>轉換至SmartPtr<Cassette>,並不比轉換至SmartPtr<MusicProduct>更好。
普通指針與const有3中結合方式:
CD goodCD("Flood"); const CD *p; CD * const p = &goodCD; const CD * const p = &goodCD;
智能指針也類似:
SmartPtr<CD> p; SmartPtr<const CD> p; const SmartPtr<CD> p = &goodCD; const SmartPtr<const CD> p = &goodCD;
但是,普通指針可以將指向non-const對象的指針轉換為指向const對象的指針,這對於智能指針卻行不通:
CD *pCD = new CD("Famous Movie Themes"); const CD *pConstCD = pCD; //可以 SmartPtr<CD> pCD = new CD("Famous Movie Themes"); SmartPtr<const CD> pConstCD = pCD; //不可以
這是因為SmartPtr<CD>和SmartPtr<constCD>是完全不同的類型。可以使用之前介紹的成員函數模板完成從SmartPtr<CD>到SmartPtr<const CD>的轉換。
實際上還有另外一種方法實現這種轉換:從non-const轉換至const是安全的,從const轉換至non-const則不安全。此外,能夠對const指針所做的任何事情,也都可以對non-const指針進行,但你還可以對後者做更多事情。同樣道理,你能夠對pointer-to-const所做的任何事情,也都可以對pointer-to-non-const做,但是面對後者,你還可以另做其他事情。
這些規則聽起來和public繼承的規則很類似。因此,可以利用這種類似性質,令每一個smart pointer-to-T class公開繼承一個對應的smart pointer-to-const-T class:
template<class T> class SmartPtrToConst { ... protected: union { const T* constPointee; T* pointee; }; }; template<class T> class SmartPtr:public SmartPtrToConst<T> { ... };
smart pointer-to-non-const-T對象必須內含一個原始的pointer-to-non-const-T指針,而smart pointer-to-const-T必須內含一個原始的pointer-to-const-T指針。一般的想法是放一個pointer-to-const-T於base class中,並放一個pointer-to-non-const-T於derived class中。然而這是一種浪費,因為SmartPtr對象會因此內含兩個pointers:其一繼承自SmartPtrToConst,其二是SmartPtr本身所有。
這個問題可通過union解決。union的訪問級別應該是protected,使兩個類都可取用。其中內含兩個必要的原始pointer類型:constPointee指針供SmartPtrToConst<T>對象使用,pointee指針則供SmartPtr<T>對象使用。當然兩個類的成員函數必須約束自己,只使用適當的指針。編譯器無法協助厲行這項規範。運用這個新設計,我們獲得了我們希望的行為:
template<class T> class SmartPtrToConst { public: SmartPtrToConst(const T*p):constPointee(p) { printf("this is SmartPtrToConst ctro, constPointee is %p, pointee is %p\n", constPointee, pointee); } void fun1() { printf("in smartptrotconst, read const ptr: %d\n", *constPointee); } protected: union{ const T* constPointee; T* pointee; }; }; template<class T> class SmartPtr : public SmartPtrToConst<T> { public: SmartPtr(T *p):SmartPtrToConst<T>(p) { printf("this is SmartPtr ctro, constPointee is %p, pointee is %p\n", this->constPointee, this->pointee); } void fun2(T a) { *(this->pointee) = a; } }; int main() { int a = 3; SmartPtr<int> sp1 = &a; SmartPtrToConst<int> sp2 = sp1; sp2.fun1(); sp1.fun2(4); printf("a is %d\n", a); sp2.fun1(); }
結果如下:
this is SmartPtrToConst ctro, constPointee is 0x7fffd103831c, pointee is 0x7fffd103831c this is SmartPtr ctro, constPointee is 0x7fffd103831c, pointee is 0x7fffd103831c in smartptrotconst, read const ptr: 3 a is 4 in smartptrotconst, read const ptr: 4
More Effective C++: 05技術(25-28)