起步篇
??? 在本文的第一部分,我們簡要介紹了ATL的一些背景知識以及ATL所面向的開發(fā)技術(shù)和環(huán)境。在這一部分 將開始走進ATL,講述ATL編程的基本方法、原則和必須要注意的問題。
??? 理解ATL最容易的方法是考察它對客戶端編程的支持。對于COM編程新手而言,一個棘手的主要問題之一是正確管理接口指針的引用計數(shù)。COM的引用計數(shù)法則是沒有運行時強制 性的,也就是說每一個客戶端必須保證對對象的承諾。
??? 有經(jīng)驗的COM編程者常常習(xí)慣于使用文檔中(如《Inside OLE》)提出的標(biāo)準(zhǔn)模式。調(diào)用某個函數(shù)或方法,返回接口指針,在某個時間范圍內(nèi)使用這個接口指針,然后釋放它。下面是使用這種模式的代碼例子:
void f(void) {
IUnknown *pUnk = 0;
// 調(diào)用
HRESULT hr = GetSomeObject(&pUnk);
if (SUCCEEDED(hr)) {
// 使用
UseSomeObject(pUnk);
// 釋放
pUnk->Release();
}
}
??? 這個模式在COM程序員心中是如此根深蒂固,以至于他們常常不寫實際使用指針的語句,而是先在代碼塊末尾敲入Release語句。這很像C程序員使用switch語句時的條件反射一樣,先敲入break再說。
??? 其實調(diào)用Release實在不是什么可怕的負(fù)擔(dān),但是,客戶端程序員面臨兩個相當(dāng)嚴(yán)重的問題。第一個問題與獲得多接口指針有關(guān)。如果某個函數(shù)需要在做任何實際工作之前獲得三個接口指針,也就是說在第一個使用指針的語句之前必須要由三個調(diào)用語句。在書寫代碼時,這常常意味著程序員需要寫許多嵌套條件語句,如:
void f(void) {
IUnknown *rgpUnk[3];
HRESULT hr = GetObject(rgpUnk);
if (SUCCEEDED(hr)) {
hr = GetObject(rgpUnk + 1);
if (SUCCEEDED(hr)) {
hr = GetObject(rgpUnk + 2);
if (SUCCEEDED(hr)) {
UseObjects(rgpUnk[0], rgpUnk[1],
rgpUnk[2]);
rgpUnk[2]->Release();
}
rgpUnk[1]->Release();
}
rgpUnk[0]->Release();
}
}
??? 像這樣的語句常常促使程序員將TAB鍵設(shè)置成一個或兩個空格,甚至情愿使用大一點的顯示器。但事情并不總是按你想象的那樣,由于種種原因項目團隊中的COM組件編程人員往往得不到 所想的硬件支持,而且在公司確定關(guān)于TAB鍵的使用標(biāo)準(zhǔn)之前,程序員常常求助于使用有很大爭議但仍然有效的“GOTO”語句:
void f(void) {
IUnknown *rgpUnk[3];
ZeroMemory(rgpUnk, sizeof(rgpUnk));
if (FAILED(GetObject(rgpUnk)))
goto cleanup;
if (FAILED(GetObject(rgpUnk+1)))
goto cleanup;
if (FAILED(GetObject(rgpUnk+2)))
goto cleanup;
UseObjects(rgpUnk[0], rgpUnk[1], rgpUnk[2]);
cleanup:
if (rgpUnk[0]) rgpUnk[0]->Release();
if (rgpUnk[1]) rgpUnk[1]->Release();
if (rgpUnk[2]) rgpUnk[2]->Release();
}
這樣的代碼雖然不那么專業(yè),但至少減少了屏幕的水平滾動。
使用以上這些代碼段潛在著更加棘手的問題,那就是在碰到C++異常時。如果函數(shù)UseObjects丟出異常,則釋放指針的代碼被完全屏蔽掉了。 解決這個問題的一個方法是使用Win32的結(jié)構(gòu)化異常處理(SEH)進行終結(jié)操作:
void f(void) {
IUnknown *rgpUnk[3];
ZeroMemory(rgpUnk, sizeof(rgpUnk));
__try {
if (FAILED(GetObject(rgpUnk))) leave;
if (FAILED(GetObject(rgpUnk+1))) leave;
if (FAILED(GetObject(rgpUnk+2))) leave;
UseObjects(rgpUnk[0], rgpUnk[1], rgpUnk[2]);
} __finally {
if (rgpUnk[0]) rgpUnk[0]->Release();
if (rgpUnk[1]) rgpUnk[1]->Release();
if (rgpUnk[2]) rgpUnk[2]->Release();
}
??? 可惜Win32 SHE在C++中的表現(xiàn)并不如想象得那么好。較好的方法是使用內(nèi)建的C++異常處理模型,同時停止使用沒有加工過的指針。標(biāo)準(zhǔn)C++庫有一個類:auto_ptr,在其析構(gòu)函數(shù)中定 死了一個操作指針的delete調(diào)用(即使在出現(xiàn)異常時也能保證調(diào)用)。與之類似,ATL有一個COM智能指針,CComPtr,它的析構(gòu)函數(shù)會正確調(diào)用Release。
???
CComPtr類實現(xiàn)客戶端基本的COM引用計數(shù)模型。CComPtr有一個數(shù)據(jù)成員,它是一個未經(jīng)過任何加工的COM接口指針。其類型被作為模板參數(shù)傳遞:
CComPtr<IUnknown> unk;
CComPtr<IClassFactory> cf;
??? 缺省的構(gòu)造函數(shù)將這個原始指針數(shù)據(jù)成員初始化為NULL。智能指針也有構(gòu)造函數(shù),它的參數(shù)要么是原始指針,要么是相同類型的智能參數(shù)。不論哪種情況,智能指針都調(diào)用AddRef控制引用。CComPtr的賦值操作符 既可以處理原始指針,也可以處理智能指針,并且在調(diào)用新分配指針的AddRef之前自動釋放保存的指針。最重要的是,CComPtr的析構(gòu)函數(shù)釋放保存的接口(如果非空)。請看下列代碼:
void f(IUnknown *pUnk1, IUnknown *pUnk2) {
// 如果非空,構(gòu)造函數(shù)調(diào)用pUnk1的AddRef
CComPtr unk1(pUnk1);
// 如果非空,構(gòu)造函數(shù)調(diào)用unk1.p的AddRef
CComPtr unk2 = unk1;
// 如果非空,operator= 調(diào)用unk1.p的Release并且
//如果非空,調(diào)用unk2.p的AddRef
unk1 = unk2;
//如果非空,析構(gòu)函數(shù)釋放unk1 和 unk2
}
??? 除了正確實現(xiàn)COM的AddRef 和 Release規(guī)則之外,CComPtr還允許實現(xiàn)原始和智能指針的透明操作,參見
附表二所示。也就是說下面的代碼按照你所想象的方式運行:
void f(IUnknown *pUnkCO) {
CComPtr cf;
HRESULT hr;
// 使用操作符 & 獲得對 &cf.p 的存取
hr = pUnkCO->QueryInterface(IID_IClassFactory,(void**)&cf);
if (FAILED(hr)) throw hr;
CComPtr unk;
// 操作符 -> 獲得對cf.p的存取
// 操作符 & 獲得對 &unk.p的存取
hr = cf->CreateInstance(0, IID_IUnknown, (void**)&unk);
if (FAILED(hr)) throw hr;
// 操作符 IUnknown * 返回 unk.p
UseObject(unk);
// 析構(gòu)函數(shù)釋放unk.p 和 cf.p
}
??? 除了缺乏對Release的顯式調(diào)用外,這段代碼像是純粹的COM代碼。有了CComPtr類的武裝,前面所遇到的麻煩問題頓時變得簡單了:
void f(void) {
CComPtr<IUnknown> rgpUnk[3];
if (FAILED(GetObject(&rgpUnk[0]))) return;
if (FAILED(GetObject(&rgpUnk[1]))) return;
if (FAILED(GetObject(&rgpUnk[2]))) return;
UseObjects(rgpUnk[0], rgpUnk[1], rgpUnk[2]);
}
由于CComPtr對操作符重載用法的擴展,使得代碼的編譯和運行無懈可擊。
??? 假定模板類知道它所操縱的指針類型,你可能會問:那為什么智能指針不能在它的功能操作符或構(gòu)造函數(shù)中自動調(diào)用QueryInterface,從而更有效地包裝IUnknown呢?在Visual C++ 5.0出來以前,沒有辦法將某個接口的GUID與它的本身的C++類型關(guān)聯(lián)起來——Visual C++ 5.0用私有的declspec將某個IID與一個接口定義綁定在一起。因為ATL的設(shè)計 考慮到了它要與大量不同的C++編譯器一起工作,它需要用與編譯器無關(guān)的手段提供GUID。下面我們來探討另一個類——CComQIPtr類。
??? CComQIPtr與CComPtr關(guān)系很密切(實際上,它只增加了兩個成員函數(shù))。CComQIPtr必須要兩個模板參數(shù):一個是被操縱的指針類型 ,另一個是對應(yīng)于這個指針類型的GUID。例如,下列代碼聲明了操縱IDataObject 和IPersist接口的智能指針:
CComQIPtr<IDataObject, &IID_IDataObject> do;
CComQIPtr<IPersist, &IID_IPersist> p;
??? CComQIPtr的優(yōu)點是它有重載的構(gòu)造函數(shù)和賦值操作符。同類版本(例如,接受相同類型的接口)僅僅AddRef右邊的賦值/初始化操作。這實際上就是CComPtr的功能。異類版本(接受類型不一致的接口)正確調(diào)用QueryInterface來決定是否這個對象確實支持所請求的接口:
void f(IPersist *pPersist) {
CComQIPtr<IPersist, &IID_IPersist> p;
// 同類賦值 - AddRef''s
p = pPersist;
CComQIPtr<IDataObject, &IID_IDataObject> do;
// 異類賦值 - QueryInterface''s
do = pPersist;
}
??? 在第二種賦值語句中,因為pPersist是非IDataObject *類型,但它是派生于IUnknown的接口指針,CComQIPtr通過pPersist調(diào)用QueryInterface來試圖獲得這個對象的IDataObject接口指針。如果QueryInterface調(diào)用成功,則此智能指針將含有作為結(jié)果的原始IDataObject指針。如果QueryInterface調(diào)用失敗,則do.p將被置為null。如果QueryInterface返回的HRESULT值很重要,但又沒有辦法從賦值操作獲得其值時,則必須顯式調(diào)用QueryInterface。
??? 既然有了CComQIPtr,那為什么還要CComPtr呢?由幾個理由:首先,ATL最初的發(fā)布版本只支持CComPtr,所以它就一直合法地保留下來了。其二(也是最重要的理由),由于重載的構(gòu)造函數(shù)和賦值操作,對IUnknown使用CComQIPtr是非法的。因為所有COM接口的類型定義都必須與IUnknown兼容。
CComPtr<IUnknown> unk;
從功能上將它等同于
CComQIPtr<IUnknown, &IID_IUnknown> unk;
前者正確。后者是錯誤的用法。如果你這樣寫了,C++編譯器將提醒你改正。
??? 將CComPtr作為首選的另外一個理由可能是一些開發(fā)人員相信靜悄悄地調(diào)用QueryInterface,沒有警告,削弱了C++系統(tǒng)的類型。畢竟,C++在沒有進行強制類型轉(zhuǎn)換的情況下不允許對類型不一致的原始指針 進行賦值操作,所以為什么要用智能指針的道理也在這,幸運的是開發(fā)人員可以選擇最能滿足需要的指針類型。
??? 許多開發(fā)人員將智能指針看成是對過于的復(fù)雜編程任務(wù)的簡化。我最初也是這么認(rèn)為的。但只要留意它們使用COM智能指針的方法。就會逐漸認(rèn)識到它們引入的潛在危險與它們解決的問題一樣多。
關(guān)于這一點,我用一個現(xiàn)成的使用原始指針的函數(shù)為例:
void f(void) {
IFoo *pFoo = 0;
HRESULT hr = GetSomeObject(&pFoo);
if (SUCCEEDED(hr)) {
UseSomeObject(pFoo);
pFoo->Release();
}
}
將它自然而然轉(zhuǎn)換到使用CComPtr。
void f(void) {
CComPtr<IFoo> pFoo = 0;
HRESULT hr = GetSomeObject(&pFoo);
if (SUCCEEDED(hr)) {
UseSomeObject(pFoo);
pFoo->Release();
}
}
??? 注意CComPtr 和 CComQIPtr輸出所有受控接口成員,包括AddRef和Release。可惜當(dāng)客戶端通過操作符->的結(jié)果調(diào)用Release時,智能指針很健忘 ,會二次調(diào)用構(gòu)造函數(shù)中的Release。顯然這是錯誤的,編譯器和鏈接器也欣然接受了這個代碼。如果你運氣好的話,調(diào)試器會很快捕獲到這個錯誤。
??? 使用ATL智能指針的另一個要引起注意的風(fēng)險是類型強制轉(zhuǎn)換操作符對原始指針提供的訪問。如果隱式強制轉(zhuǎn)換操作符的使用存在爭議。當(dāng) ANSI/ISO C++ 委員會在決定采用某個C++串類時,他們明確禁止隱式類型轉(zhuǎn)換。而是要求必須顯式使用c_str函數(shù)在需要常量char *(const char *)的地方傳遞標(biāo)準(zhǔn)C++串。ATL提供了一種隱含式的類型轉(zhuǎn)換操作符順利地解決了這個問題。通常,這個轉(zhuǎn)換操作符可以根據(jù)你的喜好來使用,允許你將智能指針傳遞到需要用原始指針的函數(shù)。
void f(IUnknown *pUnk) {
CComPtr unk = pUnk;
// 隱式調(diào)用操作符IUnknown *()
CoLockObjectExternal(unk, TRUE, TRUE);
}
這段代碼能正確運行,但是下面的代碼也不會產(chǎn)生警告信息,編譯正常通過:
HRESULT CFoo::Clone(IUnknown **ppUnk) {
CComPtr unk;
CoCreateInstance(CLSID_Foo, 0, CLSCTX_ALL,
IID_IUnknown, (void **) &unk);
// 隱式調(diào)用操作符IUnknown *()
*ppUnk = unk;
return S_OK;
}
??? 在這種情況下,智能指針(unk)對原始值針**ppUnk的賦值觸發(fā)了與前面代碼段相同的強制類型轉(zhuǎn)換。在第一個例子中,不需要用AddRef。在第二個例子中,必須要用AddRef。
??? 有關(guān)使用智能指針的更詳細(xì)一般信息,請參見Scott Meyer的《More Effective C++》(Addison-Wesley, 1995年出版)。國內(nèi)目前還沒有這本書的中譯本或影印本。有關(guān)COM智能指針的更多特定信息,請參見Don Box的一篇關(guān)于智能指針的專題文章http://www.develop.com/dbox/cxx/SmartPointer.htm。 (待續(xù))
posted on 2007-03-12 21:29
jay 閱讀(398)
評論(0) 編輯 收藏 引用 所屬分類:
ATL