昨天看了《COM本質論》的第一章”COM是一個更好的C++”,覺得很有必要做一些筆記,於是整理成這篇文章,我相信你值得擁有。
這篇文章主要講的內容是:一個實現了快速查找功能的類FastString,在一個小小的需求之后,慢慢的演變成一個COM組件的過程。
類FastString實現了一個快速查找字符串的功能,快到時間復雜度是O(1),我們先不管作者是怎么實現的,估計是通過空間換時間。由於這個類查找字符串很快,於是作者就把這個類當做一個產品,以源碼的方式賣給需要的廠商,廠商用后感覺很好,但有的廠商想要獲得字符串長度的功能,他們覺得strlen(str)速度太慢,畢竟這個函數獲取字符串的長度是線性的,時間復雜度是O(N),於是作者決定修改他的FastString,其內心一直在告訴自己:我的FastString必須是Fast。
我們先來看看作者FastString的樣子:
class FastString { public: FastString(const char* str); FastString(void ); int Find (const char* str ); private: char* m_str ; };
可別小看這個類,它查找字符串可快了(我也不知道為什么它就他媽的這么快)。聰明的作者聽了廠商的需求之后,很快的就想到了很好的解決方案,通過一個變量len來存字符串的長度,通過一個函數Length返回變量len,時間復雜度可是O(0)哦,於是作者很快的實現了廠商的需求,大概如下:
class FastString { public: FastString(const char* str); FastString(void ); int Length ();//新增的 int Find (const char* str ); private: char* m_str ; int len ;//新增的 };
在經過天衣無縫的測試之后,作者驕傲的將他的作品分發給了願意再次掏錢的廠商,廠商用了很是火大,出現了各種莫名其妙的問題,在被各個廠商咆哮之后,作者發現了他的作品的缺陷,於是決定走上COM之路。
我們先來看看廠商用了作者的FastString之后為什么就掛了呢?
廠商們拿了作者的源碼之后,就以源碼的方式和自己的其他代碼一起編譯成一個DLL文件,然后讓自己的產品升級,升級就是簡單的覆蓋這個DLL文件,於是廠商的產品升級之后就掛了。因為FastString可能在多個DLL中多個文件都實例化了,在這些DLL中FastString占用4個字節的內存,而新版本的FastString占用的是8個字節的內存,廠商只覆蓋了FastString所在的DLL,而沒有覆蓋所有使用了FastString的DLL,由於FastString所在的DLL創建FastString是8個字節,而其他DLL中是4個字節,如果跨庫傳遞FastString,將一個4字節的對象當做一個8字節的對象來用,這還不掛。
聰明的作者很快就實現了他的COM組件,源碼大概是下面這個樣子,不要奇怪為什么作者的COM之路這么順風順水,這么快就出了作品。
#pragma once class IExtensibleObject { public: virtual void* Dynamic_Cast(const char* str)=0; virtual void AddRef()=0; virtual void Release()=0; }; class IFastString:public IExtensibleObject { public: virtual int Length(void)=0; virtual int Find(const char* str)=0; }; class FastString:public IFastString { public: FastString(const char* str=NULL); virtual void* Dynamic_Cast(const char* str); virtual void AddRef() ; virtual void Release(); virtual int Length(); virtual int Find(const char* str); ~FastString(); private: char* m_str; int len; int m_cPtrs;//引用計數 }; //導出函數 extern "C" __declspec(dllimport) IFastString* CreateFastString(const char* psz);
作者的COM組件做到了一下幾點,終於實現了增量更新。
1:作者不在以源碼的方式賣給廠商,而是以頭文件和庫的方式賣個廠商,廠商可以通過靜態/動態的方式鏈接作者的庫。
2:作者不在讓廠商到處實例化他的FastString,我可愛的FastString。而是通過一個導出函數實例化FastString,並返回IFastString,這樣就不會出現不同DLL中FastString實例大小不一樣的問題。現在所有的實例都在作者的DLL中創建了。
3:關於回收FastString的問題?作者剛開始是想直接delete掉CreateFastString返回的指針,但為了實現COM組件,此時的FastString已經不是彼時的自己了,他繼承並實現了多個接口,由於接口之間轉換來轉換去,都不知道刪除哪個指針了,於是作者決定通過使用引用計數的方式銷毀FastString。
4:為什么要自己實現Dynamic_Cast?
RTTI是一個與編譯器極為相關的特征,每個編譯器廠商對RTTI的實現是獨有的,這大大破壞了“以抽象基類作為接口而獲得的編譯器獨立性”,既然每個編譯器可能有不同的實現,即析構函數不能定義成虛函數,因為不同的編譯器,虛函數在虛方法表中的位置是不一樣的,有的編譯器放在最前面有的放在最后面,這會導致不同的編譯器編譯后虛方法在虛方法表中的位置是不一樣的。所以析構函數不能定義成virtual,其他public接口都必須定義成virtual。其他虛方法在虛方法表中的位置和虛方法的聲明保持一致,即按照聲明的順序存放在虛方法中。
由於類型轉換和引用計數是每個接口都需要的,於是把他們提出來放到最頂層,讓所有的接口繼承它。
5:新增的接口只能加在最后面,廢棄的接口不能刪除。
如果新增的接口插在中間,那么部分接口在虛方法表中的地址就會發生變化,新版本的DLL就不能與已經發布的程序兼容,就不能實現增量升級,即只用覆蓋某個DLL,而不需要全部都要更新,廢棄的接口刪除會導致同樣的問題。
綜述:為什么作者的這個DLL能實現增量更新?
COM對象通過特定的導出方法在DLL中以new的方式創建,通過引用計數自動析構,客戶端不能自己創建COM對象,COM對象的內部結構發生變化,對外部也沒有影響,如果新增了接口,就在最后加,之前的接口在虛方法表中的位置不會受到印象,即對別的接口沒有影響,廢棄的接口不能刪除,
改變對象的內存結構和新增virtual方法都沒關系,那不就成了。實現增量不在是問題,我們在回到FastString這個問題上,如果FastString一開始是以上訴方式實現的,現在要新增一個len字段和一個Length接口,我就這樣增了,新出個版本,直接覆蓋以前的那個DLL,我直接可以用,一切都是OK的,外部的調用不會受到任何影響。為了證明這個FastString能實現增量升級,我做了一個DEMO,大家可以試一下,我就是下載地址。
你或許會說我這說的都不是COM,但這的確是更好的C++。