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