開發人員 : 編程
很久以前,在非常黑暗的過去,Nigel Thompson寫了一系列關於被稱為"OLE傻瓜書"的OLE編程的技術筆記。當時走廊的上上下下都能聽到他痛苦地叫喊著忘記了要正確地添加或釋放一個介面。我想本應該有一些方法在使用C++中巧妙的指標時能自動釋放元件物件模型(COM)元件,使得元件物件模型元件的使用更為容易一些。不過,我開始研究的卻是Microsoft 基礎類庫(MFC),在這個課題中引用計算並不是一個重點,因為它隱藏在MFC類之中。
在寫完文章"MFC/COM對象8:重訪無MFC的多重繼承"之後,我決定再考慮使用巧妙的指標類簡化使用元件物件模型(COM)介面。研究的結果並沒有實現我的願望,我開始懷疑是否能在我自己的代碼中使用巧妙的指標介面。不過,你的元件物件模型(COM)項目可能與我的不一樣,所以我決定不用元件物件模型(COM)元件也許並不會影響你。
在這篇文章中,我將討論下列主題:
- 建立一個靈巧的介面指標類的原因
- 如何建立一個巧妙的介面指標類
- 使用巧妙的介面指標類
- 我不喜歡巧妙的介面指標類的原因
在這篇文章的原始碼中,我使用首碼PI指示一個指向介面的指標,例如:
IPersist* pIPersist ;I use the prefix SI to refer to a smart interface pointer:CSmartInterface SIPersist ;
建立一個巧妙的介面指標類的原因
我想要一個巧妙的介面指標類的原因是要自動地添加和釋放介面指標。
使用元件物件模型(COM)介面指標時,你必須要遵循幾條規則。首先,絕不要在一個介面指標上調用刪除(delete)來代替Release。下面的代碼是不正確的:
IDraw* pIDraw ;CoCreateInstance(...,IID,_IDraw, (void**)&pIDraw) ;pIDraw->Draw(0,0,100,100) ;delete pIDraw ; // Don't delete an interface pointer. |
下面的代碼是正確的:
IDraw* pIDraw ;CoCreateInstance(...,IID,_IDraw, (void**)&pIDraw) ;pIDraw->Draw(0,0,100,100) ;pIDraw->Release() ; |
C++程式設計員通常delete一個對象指標。因為這個原因,C++程式員容易忘記並且在一個介面指標上用delete代替Release。它也是C++程式員很難發現的一個錯誤,因為刪除指標太自然了。
第二條規則是在建立新的指標時調用AddRef 。下面的代碼是不正確的:
IDraw* pIDraw ;CoCreateInstance(...,IID,_IDraw, (void**)&pIDraw) ;pIDraw->Draw(0,0,100,100) ;delete pIDraw ; // Don't delete an interface pointer. |
下面的代碼是正確的:
IDraw* pIDraw ;CoCreateInstance(...,IID,_IDraw, (void**)&pIDraw) ;pIDraw->Draw(0,0,100,100) ;pIDraw->Release() ; |
上面的例子很小,但是,在複雜的代碼中,這個錯誤是很難跟蹤到的。
使用巧妙的介面指標類並不只有唯一一個原因。DonBox在C++報道中提到了其他的一些原因(請參閱本篇文章末尾的書目)。
如何建立一個巧妙的介面指標類
一個巧妙的介面指標類開始建立的方式與一個巧妙的指標類是一樣的:通過為一個類執行操作符-》。這個處理過程也可被稱為委派。通過覆蓋操作符->,我們可以做一個類模仿一個指標調用。例如:
void Draw(){ CSmartInterface<idraw> SIDraw; CoCreateInstance(...,IID,_IDraw, (void**)&SIDraw) ; SIDraw->Draw() ;} |
在上面的代碼中有幾點需要注意。首先,一個模板類被用於執行巧妙的介面指標。這使得巧妙的指標介面類是個安全類型。第二,就象我們就要看到的,操作符-已經被超載去返回一個包括在CsmantInterface中的指標的地址。第三,即使SIDraw不是一個指標,我們也使用操作符->去調用Idraw介面中的成員。第四,我們不調用Release是因為在棧上已經建立了CxmartInterface,解構函式會自動調用Release。
下面是CsmartInterface標頭檔中重要的部分,所以的成員函數和操作符都在標頭檔中稍後的部分中定義了。
CsmartInterface包含著一個指向一個介面的指標。為了型別安全,它被定義為一個模板函數。CsmartInterface的實質是超載操作符->。
template <class i>class CSmartInterface{public: // Construction CSmartInterface(I* pI = NULL) ; // Copy Constructor CSmartInterface(const CSmartInterface<i>& rSI) ; // Destruction ~CSmartInterface() ; // Assignment from I* CSmartInterface& operator=(I* pI) ; // // Operators // // Conversion operator I*() ; // Deref I* operator->() ; // Address of I** operator&() ; // Equality BOOL operator==(I* pI) const; // Inequality BOOL operator!=(I* pI) const; // Negation BOOL operator!() const ; protected: I* m_pI ;}; |
所以,來自前一個例子的SIDraw->Draw()導致了對SIDraw.m_PI->Draw()的調用。SIDraw把Draw調用委派給m-PI指向的介面。這種方式的強大之處在於CsmartInterface類不需要在每個新函數被增加到Idraw介面時被改變 。不過,就象我們看到的,CsmartInterface不能停止對Idraw介面的個別調用。
為了使CsmartInterface成為純粹的C++指標的一個更為令人可信的類比,處理操作符 ->還需要定義別的操作符。事實上,做一個巧妙的指標類的最困難的事情是確保所有用在指標上的操作符都已經定義並且有意義。例如:當我把下面的代碼從if(PISimple==NULL)轉變成if (SISimple == NULL)。。。。。。,我就必須為我的巧妙的指標類 定義操作符==。產生的程式碼編譯沒有錯誤,不過,它包含著一個錯誤因為它把NULL 與SISimple比較而不是和我已經擴充的SISimple.m_PI比較。在我定義了操作符==後,這個錯誤消失了。你喜愛的C++編程書的目錄應該列出了所以你需要定義的操作符,從而充當一個方便的查詢表。為了安全起見,我定義了我認為我不需要的操作符--這樣如果我視圖用他們就會得到一條錯誤資訊。類似於C/C++使用者月刊中的Robert Mashlan的"C++中被查過的指標 "的文章能真正協助你理解巧妙的指標。超載絕大部分需要的操作符使很直接明白的。最有趣的是操作符=:
template <class i> inline CSmartInterface<i>& CSmartInterface<i>::operator=(I* pI){ if (m_pI != pI) //OPTIMIZE: Same pointer AddRef/Release not needed. { if (m_pI != NULL) m_pI->Release() ; //Smart Pointers don't use smart pointers :-) m_pI = pI ; if (m_pI != NULL) m_pI->AddRef() ; } return *this ;} |
操作符=會自動地為你添加和釋放介面。如果CsmartInterface已經指向一個介面,它將會釋放它並且添加新的介面。這個操作符=的定義允許下列操作:
void DrawThemAll(){ CSmartInterface<idraw> SIDraw ; for (int i = 0 ; i < MAX ; i++ ) { SIDraw = pIDraw[i] ; SIDraw->Forward(x) ; }} |
上面的代碼依靠CsmartInterface的解構函式去釋放指標,方法是:
template <class i> inline CSmartInterface<i>::~CSmartInterface(){ if (m_pI != NULL) { m_pI->Release(); }} |
我們使用恰當的建構函式可以更好地利用解構函式
template <class i> inlineCSmartInterface<i>::CSmartInterface(I* pI /*=NULL*/) : m_pI(pI){ if (m_pI != NULL) { // AddRef if we are copying an existing interface pointer. m_pI->AddRef() ; }} |
現在我們的例子可以改為:
void DrawThemAll(){ for (int i = 0 ; i < MAX ; i++ ) { CSmartInterface<idraw> SIDraw(pIDraw[i]) ; SIDraw->Forward(x) ; }} |
上面的代碼運行過一個Idraw介面指標的列表,添加他們,使用他們和釋放他們。
關於這一點你還可以作的更多。DonBox在他的專題裡把這一點闡述得更為深入。他定義了他的CsmartInterface的等價物去接受介面的ID和類型。然後他定義了一個建構函式,一旦有了來自不同介面的賦值,這個介面函數將調用Query Interface:
CSmartInterface(IUnknown* pI) : m_pI(NULL){ if (pI != NULL) pI->QueryInterface(*piid, (void**)&m_pI) ;} |
上面的建構函式允許我們把例子改為:
void DrawThemAll(){ for (int i = 0 ; i <<IDraw, &IID_Draw> MAX ; i++ ) { CSmartInterface SIDraw(pIUnknown[i]) ; SIDraw->Forward(x) ; }} |
這個例子開始表現這種技術的強大之處,上面的代碼類似於:
void DrawThemAll(){ IDraw* pIDraw ; for (int i = 0 ; i < MAX ; i++ ) { pIUnknown[i]->QueryInterface(IID_Draw, (void**)&pIDraw) ; pIDraw->Forward(x) ; pIDraw->Release() ; }} |
操作符=也能用同樣的方式擴充,所以類似於SIDraw=PIUnknown;這樣的賦值將會調用QueryInterface。我並不熱衷於把執行藏在看起來無害的操作符後 ,儘管我不得不說用這種方式超載操作符=是非常令人信服的方式。Visual Basic 4.0版本在把一個元件物件模型分配給另一個時就要調用QueryInterface。
使用巧妙的介面指標類
使用CsmartInterface有兩個主要的規則。首先,不要在CsmartInterface對象上調用Release。
void Draw(){ CSmartInterface<idraw> SIDraw; CoCreateInstance(...,IID,_IDraw, (void**)&SIDraw) ; SIDraw->Draw() ; SIDraw->Release() ; // Will compile, but is a bug.} |
當SIDraw->Release被調用,則SIDraw->m_pl被釋放。當SIDraw->m_PI的解構函式被調用時,只要介面還沒有 被釋放,SIDraw->m_PI將再一次被釋放。這個問題並不是難以調試,如果你有關於引用計數的問題,你可以尋找一下所有發生的Release。另一個途徑是適用#define Release BOGUS_DO_NOT_CALL_RELEASE!但是這樣使用Release則會產生一個錯誤。當然,如果你的應用程式裡其他帶有"Release"這個詞的函數(就象許多Win32應用程式介面[APIS],這個方法將行不通。
第二條規則是避免把CsmartInterface對象使用成指標。如果你這樣做,情況將很混亂:
CSmartInterface<isimple>* pSISimple = new CSmartInterface<isimple>(m_pISimple) ;(*pSISimple)->Inc() ;delete pSISimple ; |
使用typedef將能將這種情況稍稍整理一下:
typedef CSmartInterface<isimple> CSmartISimple ;CSmartISimple* pSISimple = new CSmartISimple(m_pISimple) ;(*pSISimple)->Inc() ;delete pSISimple ; |
不過,這並不能改變(*PSISimple)->Incc);實際上不是非常簡單的事實。
如果我們考慮一下為什麼我們想要一個指向介面的指標,我們可能會發現環繞著問題的一條路。我們想在程式中的一些任意點釋放介面--而不是要等到CsmartInterface已經不在範圍之內。那麼問題將變成我們如何釋放包含在CsmartInterface中的介面。答案非常的簡單:SISimple=NULL;雖然這是一句很典型的C代碼,但是這兒到底發生了什麼卻一點都不明顯。
pISimple->Release() ;anddelete pSISimple ; |
都是更明顯地指示對象已經消失的方法。
我不喜歡巧妙的介面指標類的原因
我為什麼不打算使用巧妙的指標類有幾點原因。所有這些理由的要點是CsmartInterface並不象C++。在一個對象上使用操作符->而不是指向對象的指標,這真的很奇怪。
一個相關的原因是使用指向CsmartInterface的指標並不直截了當--事實上它非常混亂。我的絕大部分組件物件模型使用隱藏的介面指標的容器,而且介面的生命週期也很少被限制在函數的範圍內。我為一個介面調用QueryInterface,把它儲存在容器裡,使用它,最後釋放它--所以這些都來自My Code中的不同地方。使用這種結構類型,我需要分配和釋放棧上的巧妙介面,而這就象我在前面章節中說明的那樣非常混亂。
一個C++的程式員可能要刪除一個介面指標,而一個元件物件模型(COM)程式員則可能要釋放一個CsmartInterface對象。沒有什麼方便的方法可以阻止這一點。所以,我們的解決方案已經替換了一個帶有類似相等問題的問題。當然,至少目前為止,C++程式員的人數還是要比元件物件模型(COM)程式員的人數要多。
我已經決定使用介面封裝代替巧妙的介面指標。在我的"用介面封裝調用元件物件模型(COM)對象"一文中對介面封裝進行了描述。
總結
巧妙的介面指標是一種強有力的技術,它使得用元件物件模型(COM)對象工作更為簡單並且更加bug_free。不過,我發現巧妙的介面指標是一種非常奇怪的東西。他們既不是純粹的指標也不是純粹的對象。他們也不能象我所期望的那樣適合我的應用程式結構。不過,我還是強烈建議你試試在你的應用程式裡使用巧妙的介面指標,看看他們是如何為你工作的。他們可能只是你需要讓你的應用程式更快且問題更少地技巧而已。