2018年5月26日
String類的設計一點都不容易,先不論C++,那怕是其他語言,在面對string的時候,一不小心也會掉坑,好比java,好比C#,一開始假設utf16是定長編碼,后來Unicode發展到兩個字節就裝不下一個碼位,字符串在java下,就有點尷尬了。就算是昧著良心用utf32編碼,碼元與碼位終于一一對應了,也會遇到物理字符與邏輯字符不對應的時候,好像有些語言的字符要用兩個unicode值來表示(很奇怪),有些語言的一個小寫字符對應著好幾個大寫字符。即便是字符串選定了一種編碼方式,始終還是要解決與其他編碼的交互問題,這些交互接口也不容易設計。另外,每次從長字符串中截取字符串都要重新new出來一條新的字符串,難免有一點點浪費,當然,現在計算機性能過剩,這純粹是強迫癥。
而到了c++下,設計字符串所遇到的問題,就遠比想象中復雜,無中生有的又憑空多出來很多不必要的要求,內存資源管理(這個在C++幾乎是無解),異常安全(往字符串添加新內容,假如內存分配失敗,必須保持原有值的完整性),還有性能要求(截取字符串避免生成新的字符串)。很多很多的要求,導致語言層面上壓根就沒法也不可能提供原生的字符串支持,而這一點上又引出來新的問題,代碼里面,邏輯意義上看,就不止存在一種字符串類型。好在,大C++擁有豐富多彩的feature,應該足以實現字符串類型了,這也是大C++的設計哲學,既然語言上沒法實現的東西,就提供用以支持這種東西的feature,用戶要怎么實現就怎么實現,選擇權交到用戶手里。
所以,C++的庫要怎么做出來一道string,這道菜的味道如何,就很讓人好奇。一路考察下來,讓人大跌眼鏡,竟然沒有一個c++庫能提供品質優良字符串, 其抽象充其量也就是比字符數組好一點點,完全就沒有Unicode編碼的抽象。Stl的字符串更讓人發指,竟然有幾個模板參數,本來多類型的字符串問題就更是雪上加霜了,另外stl的string還不能作為dll函數的參數類型。其實,很多時候,猿猴的要求真的不高,只要求一種utf8編碼的string,帶有格式化,還有一些split,trim,FindOneOf,toupper等常用字符串處理的操作就行了,只可惜,沒有一個c++庫能基本滿足這樣的基本要求。其實,這些要求,具體到C++下,要基本滿足,也的確很困難。
除了c++,很多語言的string類型都是原子屬性,一個string值,但凡一點風吹草動,都要生成新的string值,原有的值必須保持不變。此外,其官方也提供了類似于StringBuffer或者StringBuilder用以構造很長很長,以彌補這種動不動就生成新String的性能問題。這兩種類型涇渭分明。而c++的string,似乎是把這兩種類型糅合在一塊了,由此帶來語義上的不清晰,也造成很多不必要的麻煩,因為絕大多數場合下,只需要使用string的原子屬性,可變的string只是用來保存字符緩沖而已。知道嗎,stl的string有一百多個成員函數,很多都是不必要的重載,不過是為了避免字符串的復制而已。
所以,首先要對只讀的string做抽象,也即是string_view,只需兩個成員字段,字符串的起始地址以及緩沖長度,并且不要求以0結束,它有一個很好的特性,字符串的任何一部分,也都是字符串,甚至,必要時,一個字符,通過取地址,也可以看做是長度為1的string_view。任何連續的內存字符塊,都可以看做是string_view。其不必涉及內存的分配,顯得非常的輕量級,可以在程序中到處使用,只需注意到字符緩沖的生命周期,就不必擔心會引來什么問題。在string_view上,可以做trim,比較,查找,反向查找等操作,除了讀取單個字節的迭代器,還提供兩套迭代器,用以取到unicode碼位值(uin32),和用以訪問邏輯字符,其值也為stirng_view。
剩下來就是可寫可修改的string,要求以0結束,也即是stl的string,因為很多函數都在string_view上,所以這里基本上都只是插入、添加、刪除、替換的操作,要注意的是,中括號操作符不能返回字符引用,因為那樣完全沒有任何意義,就算是保留中括號返回字符值,意義也很小。Trim、查找、比較等操作,必須通過其成員函數view來返回代表自己的string_view。String的很多成員函數,大多數參數類型就是string_view,因此也沒有像是在stl下垃圾string的那么多亂七八糟的重載。很簡明的設計,性能與簡單的良好統一,不知為何,stl要到c++17的時候,才會加入stirng_view這么重要的類型,即便是如此,stl的string既有代碼已成定局,也沒辦法用string_view來簡化它的一百多個的成員函數了
2018年5月22日
近兩年來在寫C++的運行時環境,反射、運行時類型信息、內存管理、并行、字符串、協程、ORM等等,基本上重寫了一套標準庫以及運行庫。對于在c++下使用字符串,深有體會。一開始嘔心瀝血,殫精竭慮,支持多種編碼方式(Utf8、Utf7、GB2312、Utf16LE,Utf16BE等)的字符串類型,以及在此之上的對這些字符串提供格式化、字符串解析、json、xml、文件讀寫BOM等等功能,必須承認,大C++真是變態,像是這樣變態無聊的概念都可以支持,還可以實現得很好,用起來確實也方便。可是,每次面臨字符串操作的時候,都會心里發毛,都會嘀咕此時此刻,糾結的是哪門子的編碼,也搞得很多代碼必須以template的形式,放在頭文件上,不放在頭文件,就必須抽象出來一個通用的動態字符串類型,代表任意編碼的一種字符串類型,代碼里面引入各種各樣臆造的復雜性。終于受不了啦,最后搞成統一用utf8編碼,重構了幾千行代碼(十幾個文件),然后,整個字符串世界終于清靜了,接口api設計什么的,也一下子清爽了很多。整個程序內部,就應該只使用同一種編碼的字符串。stl的帶有多個模板的string設計,就是無病呻吟,畫蛇添足。
為什么選擇Utf8編碼,首先,非unicode編碼的字符串是不能考慮的;其次,utf16也是變長的編碼方式,而且還有大小端的區別,所以也不能考慮;utf32又太占用內存了。想來想去,終于下定決心,utf8簡直就是唯一的選擇了。雖然可能有這樣那樣的小問題,比如說,純中文文本,utf8占用多50%內存(相比于Utf16),windows下utf8有點不友好。但其實都不是問題,也都可以解決。比如說,windows下,所有的涉及字符串與系統的api交互,先臨時轉換成utf16,然后再調用api。api的返回結果為utf16,再轉換為utf8。好像有一點性能上的損失,其實沒啥大不了的。windows對于多字節也是這樣支持的,完全就感受不到性能上的影響。總之,utf8簡直就是程序處理的唯一字符串編碼。
吐槽一下std的字符串,以及與此相關的一切概念,iostream,locale等等東西,垃圾設計的典范。接口不友好,功能弱,而且還性能差,更關鍵的是其抽象上的泄漏。一整天就只會在引用計數,寫時復制,短字符串優化上做文章,時間精力都不用在刀刃上。C++17終于引入string_view的類型,情況稍微有些改善。由于字符串使用上不方便,也因此損失了一大片的用戶,陣地一再失守。整體上講,stl的設計,自然是有精心的考慮,但是,作出這些抽象的標準會上一大群的老爺子們,大概率上講,應該是沒有用stl正兒八經地開發工業級上的代碼,臆造抽象,顧慮太多,表面上看起來好像是那么一回事,真正用起來的時候,就不太對勁,會有這樣那樣的不足,很不方便。
簡單說一下U8String的設計思路。U8String用以管理字符串編碼緩存的生命周期,追加縮短替換字符串,支持通過下標可以讀取字節char,但是不支持將字節寫入到某個索引上的位置,當然支持往字符串中插入unicode編碼的字符。至于字符串的比較、查找、Trim、截取子字符串這些常用操作,就全部壓在U8View上。如果U8String要使用這些,要先通過view的函數,獲取自己字節緩存下的視圖。U8View表示一段連續的字符編碼內存,U8View的任意一部分也是U8View,不要求以0結束。只要求U8View的生存周期不能比其宿主(U8String,字符數組,U8原生字符串)長命。事實上,很多api的字符串參數,其實只是要求為U8View就行了,不需要是什么const string&類型。此外,還提供U8PointPtr的指針類型,用以遍歷U8View,其取值為unicode編碼值,也就是wchar_t類型。另外,既然有U8View,自然也就有ArrayView,代表連續內存塊的任意類型。
自然,庫中必須提供格式化Fmt以及解析字符串Scanf的函數。StrFmt用以生成新的U8String,而Fmt格式化函數中傳入字符串的話,就將格式化結果追加到字符串后面。Fmt可以格式化數據到控制臺,文本文件,日志等等輸出結果上。StrFmt的實現只是簡單地調用Fmt并返回U8String。有了Fmt和Scanf,操作字符串就很方便很靈活了,同時也消除很多很多有關字符串相關的處理函數。Fmt不僅僅能格式化基本類型,自定義類型,還能格式化數組,vector,list,pair,tuple等模板類型的數據。庫中也提供了類似于iostream重載<<和>>的操作符。大C++提高的feature,造出來的string類型,使用上的方便,一點都不遜色于其他任何語言的原生string類型。當然,std的那個string,簡直就是廢物。
不管怎么說,本人還是很喜歡C++的,用c++寫代碼很舒暢,可比用C#、haskell、lisp、scala時要開心很多。C++發展到C++11,基本功能也都完備了,當然,C++14、C++17自然功能更加強大,特別是實現模板庫的時候,就更方便了,也確實很吸引人。自然,C++也非十全十美,也有很多的不足,比如不能自定義操作符,不提供非侵入式的成員函數,缺乏延遲求值的語言機制,引用的修改綁定(只要不綁定到nullptr就好了),成員函數指針的無端限制。但是,世界上又哪里存在完美的language呢,特別是對于這種直接操縱內存的底層語言來說。至于rust,叫囂著要取代c++,就它那副特性,還遠著呢。
2017年12月13日
大家都知道,大C++里面可以私有繼承,之后基類的一切,在子類中就成為private的了,不對外開放了。現在流行接口,組合優化繼承,所以private繼承這玩意,日漸式微,很久以前就很少使用了,嗯,不要說private,就算是大c++,也是江河日下。不過,存在即合理,c++語法里面的任何東西,都有其價值,平時可以用不到,但是關鍵時刻用一下,確實很方便,當然多數情況下,也可以其他途徑來完成,但是,就是沒那么舒服。
廢話就不說了,直入正題吧。
假設,現在有接口,假設是IUnknown,里面有那三個著名的純虛函數,QueryInterface, AddRef, Release,好像是這三個哥倆。
然后,有一個類,就叫ClassA,實現了IUnknown接口,其實就是繼承IUnknown,也就是說,重寫了那三個純虛函數。此外,ClassA還有一大堆自己的東西,比如public的字段或者成員函數。
現在,有ClassB,想基于ClassA來做一些事情,但是又不想讓用戶看到ClassA里面那些亂七八糟的玩意,因此,這種情況下,用private似乎很合適。代碼如下:
struct IUnknown
{
public:
virtual HRESULT QueryInterface(REFIID riid,void** ppvObject) = 0;
virtual ULONG AddRef() = 0;
virtual ULONG Release() = 0;
};
struct ClassA : IUnknown
{
virtual HRESULT QueryInterface(REFIID riid, void** ppvObject) override { ... }
virtual ULONG AddRef() override { ... }
virtual ULONG Release() override { ... }
...
};
struct ClassB : private ClassA
{
...
};
這里,內存的使用上非常緊湊,可以說,沒有多余的地方。但是,這里的private,不僅僅會private ClassA的一切,就連IUnknown也被private,這有時候就不符合要求了,因為這里意圖是,private ClassA,但是又想public IUnknown,也就是說,對外界來說,ClassB不是ClassA,雖然其內部基于ClassA實現,但是,又希望ClassB是IUnknown。對此,有幾種解決做法,但是都不能讓人滿意。
方法1、讓ClassB再次實現IUnknown接口,如下所示:
struct ClassB : private ClassA, public IUnknown
{
virtual HRESULT QueryInterface(REFIID riid, void** ppvObject) override { ... }
virtual ULONG AddRef() override { ... }
virtual ULONG Release() override { ... }
};
其好處是,ClassB的實例可以無縫用于IUnknown的一切場合,不管是引用或者指針,const非const。但是,代價也是很大的,首先要針對IUnknown的每個虛函數,都要一一手寫,再次轉發給private的基類,其次,ClassB比ClassA多了一個虛函數表指針,大小就比原來多了一個指針的大小,這就不是零懲罰了,這是最不該。
方法2,還是保持私有繼承,再在ClassB中添加幾個函數,用以返回IUnknown,代碼如下
struct ClassB : private ClassA
{
//也可以using ClassA的三個IUnknown里面的函數
const IUnknown* GetUnknown()const { return this; }
IUnknown* GetUnknown()const { return this; }
};
避開了方法1的不足,但是就不能無縫用于IUnknown下,每次使用必須調用一下GetUnknown(),對于引用的情況下,還必須加多一個星號*,也是挺不方便的。對了,這里就算添加了類型函數重載,也即是operator IUnknown,編譯器也拒絕將ClassB無縫轉換成IUnknown。
方法3,用包含,不用私有繼承。如下:
struct ClassB
{
ClassA mA;
operator const IUnknown&()const { return *this; }
operator IUnknown&() { return *this; }
};
這樣子,ClassB的實例可以無縫用于IUnknown引用下的情況。對于指針的話,可以仿造方法2那樣子,寫兩個函數進行調用。貌似綜合起來,方法3的整體分數最高。
就個人而言,更偏向于,直接就讓ClassB public繼承ClassA好了,少了那么多鬼怪,雖然出現很多不必要的函數,其實也沒什么不好。
2017年7月15日
本人對于c++的認識,多年下來,經歷了以下幾個階段,
1、 c++很好很強大,盲目追求運行性能,簡直巴普洛夫條件反射,貢獻了一大坨垃圾代碼;
2、 c++的面向對象對持很垃圾,什么鬼,代碼很容易就耦合,于是迷上對象+消息發送的面向對象;
3、 c++太復雜了,template太抽象,天外飛仙,搞不懂,二進制復用又差。整個c++就是垃圾,簡直程序設計語言里面的敗類,C語言多好啊,小巧精致簡單清晰;
4、 使用其他語言做開發,java、C#、F#、elisp、scheme、python、haskell、javascript、php等等一大坨語言,感概每一種語言都比垃圾C++不要好太多,發誓不再用c++寫哪怕一行的代碼;
5、 某一天,突然有點理解了這種語言,一切變得清晰了,原來c++也相當不錯,也可以做一些事情,看開之后,感覺開發效率也跟上來了,做同樣的事情,用c++實現不會比C#、python等慢。
相比于其他語言,c++的獨特優勢在于
預編譯期的偽圖靈完備,這一點,好多語言還是有的,并且更超級好,比如rust,scheme
編譯期間的C++是功能完備的解釋器,其輸出結果是正常運行的c++代碼,結合宏,可以制造很多其他語言必須在語法層面上支持的語法糖。這個解釋器的奇妙之處在于它運行于編譯期,一旦錯誤的模板代碼要進入運行期,就會出現編譯錯誤,而不需要進入運行時的代碼,即便天大錯誤,也都不要緊,而一旦這段代碼要進入運行時,那么模板錯誤就逃不過編譯期解釋器的法眼。
生成各種內存布局的便利語法糖和自由的內存操控;不同類型的對象,只要其內存布局一致,通過強制轉換,就可按同一類型來處理,這一點作死能力,絕不被有gc的語言支持。內存的無節操玩弄,結合template,分分鐘就能仿真出來其他必須語言層面上提供的數據結構,類型安全、運行性能、易用性,一點都不遜色,好比string,委托,元組,列表,可空類型;
C++的專有特性,raii、多繼承和全局變量。特別是全局變量,結合它的構造函數特點和類型推導,所能玩出來的豐富新花樣,其他語言很難做到。全局變量是連接運行期和編譯期的橋梁。如果沒有全局變量,本座應該不會再次對c++產生熱情。奇怪的是,至今為止,c++的基礎庫都不怎么挖掘全局變量的潛能。當然,對全局變量的使用,肯定是把它當做常量來用,全局變量有唯一的內存地址,就起到原子的作用,但它又可打包了豐富的靜態類型信息。
以上的獨特,造就了c++層出不窮的新意,而卓越的運行性能,只是其微不足道的優點。雖然說,語言不重要,思想才重要,軟件架構才重要,但是c++所能承載的思想,以及其到達的抽象高度,的確就真的大大降低框架的復雜性,誠然,c++的基礎庫開發要面臨無窮無盡的細節糾結,其實,這也反映了c++編譯器掌控細節的能力,因此,我們又可以讓編譯器自動完成很多很多細節重復,從而大幅度地減輕代碼數量,還無損其運行性能。又由于c++完備強大的靜態類型特性,在用動態語言風格的簡潔來編寫代碼的同時,又無損其快速方便地代碼重構。筆者的基礎庫項目,幾十次大規模的重構,借助單元測試,保證了重構順利快速的完成,深感c++在重構上的便利,這些代碼,包括不到1千行卻功能完整的xml庫(還支持對象與xml數據的直接互相轉換);不到1千行卻一點都不遜色于boost的spirit組合子解釋器(編譯速度卻快了很多,語法上簡潔很多,更能方便地解釋各種語法);才1千多行的異步io框架;輸入輸出,文件操作,數據庫,協程等代碼都簡潔異常,所有這些代碼都支持動態庫上的二進制復用,讓人很驚詫于c++的光怪陸離的強大。
當然,c++的缺陷也震撼人心,
1、 語言特性太過繁雜抽象微妙,比如template、多繼承、運算符重載、類型轉換、兼容性考慮的很多糟糕語言特性,所以對使用者的節制力要求很高,要求他們時刻清楚自己在干什么,瑣碎上的思考太多;
2、 缺乏統一的二進制標準,基礎庫都用源代碼的形式共享,這讓原本就龜速的編譯速度更加地令人大大感動;
3、 缺乏高標準的基礎庫,stl和boost更在某些技術運用的展示上更起到很壞的影響;
4、 缺乏某些延遲求值的機制,缺乏必要的函數式語言機制,所以c++始終就無法成為堂堂正正的現代化高級語言!
就這樣吧。
終于寫到c++的非侵入式接口了,興奮,開心,失望,解脫,…… 。在搞了這么多的面向對象科普之后,本人也已經開始不耐煩,至此,不想做太多闡述。
雖然,很早就清楚怎么在c++下搞非侵入式接口,但是,整個框架代碼,重構了十幾次之后,才終于滿意。支持給基本類型添加接口,好比int,char,const char*,double;支持泛型,好比vector,list;支持繼承,基類實現的接口,表示子類也繼承了對該接口的實現,而且子類也可以拒絕基類的接口,好比鴨子拒絕基類鳥類“會飛”,編譯時報錯;支持接口組合;……,但是,這里僅僅簡單介紹其原理,并不涉及C++中各種變態細節的處理,C++中,但凡是要正兒八經的稍微做點正事,就要面臨無窮無盡的細節糾結。
先看看其使用例子:
1、自然是定義一個接口:取之于真實代碼片段
struct IFormatble
{
static TypeInfo* GetTypeInfo();
virtual void Format(TextWriter& stream, const FormatInfo& info) = 0;
virtual bool Parse(TextReader& stream, const FormatInfo& info)
{
PPNotImplement();
}
};
2、接口的實現類,假設為int添加IFormatble的接口實現,實際代碼肯定不會這樣對一個一個的基本類型來寫實現類的代碼。這里只是為了舉例說明。類的名字就隨便起好啦,
struct ImpIntIFormatble : IFormatble
{
int* mThis; //這一行是關鍵
virtual void Format(TextWriter& stream, const FormatInfo& info)override
{
}
virtual bool Parse(TextReader& stream, const FormatInfo& info)override
{
}
};
這里的關鍵是,實現類的字段被規定死了,最多只能包含3個指針成員字段,且第1個字段一定是目的類型指針,第二是類型信息對象(用于泛型),第三是額外參數,次序不能亂。成員字段如果不需要用到第二第三個成員字段數據,可以省略不寫,好比這里。所有接口實現類必須遵守這樣的內存布局;
3、裝配,將接口的實現類裝配到現有的類上,以告訴編譯器該類對于某個接口(這里為IFormatble)的實現,用的是第2步的實現類ImpIntIFormatble;
PPInterfaceOf(IFormatble, int, ImpIntIFormatble);
4、將實現類注冊到類型信息的接口實現列表中,這一步可以省略,只是為了運行時的接口查詢,相當于IUnknown的Query。這一行代碼是在全局對象的構造函數中執行的,放在cpp源文件中
RegisterInterfaceImp<IFormatble, int>();
然后就可以開開心心地使用接口了,比如
int aa = 20;
TextWriter stream(
);
FormatInfo info(
);
TInterface<IFormatble> formatable(aa); //TInterface這個名字過難看,也沒辦法了
formatable->Format(stream, info);
double dd = 3.14;
formatable = TInterface<IFormatble>(dd); //假設double也實現IFormatble
formatable->Format(stream, info);
是否有點神奇呢?其實也沒什么,不過就是在trait和內存布局上做文章,也就只是用了類型運算的伎倆。考察ImpIntIFormatble的內存布局,對于普遍的c++編譯器來說,對象的虛函數表指針(如果存在的話),都放在對象的起始地址上,后面緊跟對象本身的成員數據字段,因此,ImpIntIFormatble的內存布局相當于,
struct ImpIntIFormatble
{
void* vtbl;
int* mThis;
};
注意,這里已經沒有繼承了。這就是,實現了IFormatble 接口的ImpIntIFormatble對象的內存表示。因此,可以想象,所有的接口實現類的內存布局都強制規定為以下形式:
struct InterfaceLayout
{
const void* mVtbl;
const void* mThis; //對象本身
const TypeInfo* mTypeInfo; //類型信息
const void* mParam; //補充參數,一般很少用到
};
當然,如果編譯器的虛函數表指針不放在對象起始地址的話,就沒法這么玩了,那么非侵入式接口也無從做起。然后,就是TInterface了,繼承于InterfaceLayout
template<typename IT>
struct TInterface : public InterfaceLayout
{
typedef IT interface_type;
static_assert(is_abstract<IT>::value, "interface must have pure function");
static_assert(sizeof(IT) == sizeof(void*), "Can't have data");
public:
interface_type* operator->()const
{
interface_type* result = (interface_type*)(void*)this;
return result;
}

};
不管怎么說都好,TInterface對象的內存布局與接口實現類的內存布局一致。因此操作符->重載函數才可以粗暴的類型轉換來順利完成。然后構造TInterface對象的時候就是強制獲取ImpIntIFormatble對象的虛函數表(也就是其起始地址的指針數據)指針賦值給InterfaceLayout的mVtbl,進而依次把實際對象的指針放在mThis上,獲取到類型信息對象放在mTypeInfo中,如果有必要搭理mParam,也相應地賦值。
然后,就是template<typename Interface, typename Object>struct InterfaceOf各種特化的運用而已,就不值一提了。
由于c++的abi沒有統一標準,并且,c++標準也沒有規定編譯器必須用虛函數表來實現多態,所以,這里的奇技淫巧并不能保證在所有平臺上都能夠成立,但是,非侵入式接口真是方便,已經是本座寫c++代碼的核心工具,一切都圍繞著非侵入式接口來展開。
原本打算長篇大論,也只有草草收場。之后,本座就解放了,會暫時離開cppblog很久,計劃中的內容,消息發送,虛模板函數,字符串,輸入輸出,格式化,序列化, locale,全局變量,模板表達式,組合子解析器,allocator,智能指針,程序運行時,抽象工廠訪問者等模式的另類實現,以求從全新的角度上來表現C++的強大,也只能中斷了。
如果說,類的設計思路,是以數據為基礎的縱向組織結構,只有唯一的分類方式,有相同基類的,就意味著其相似性,共同點都體現在基類上;那么,接口就是以功能以性質從橫向上,來看待類的相似性,并且存在無數的橫向視角(否則就失去意義)。
靜態面向對象語言,這里不考慮template,c++的template是鴨子類型,本質上,c++編譯期就是一個功能完備的動態語言。代碼上的復用就只能以基類為粒度來進行,比如,函數int fn(Base* bb),只有Base的子類,才有資格成為函數fn的會員。函數fn之所以聲明其變量bb的類型為Base,就是為了使用類型Base里面的一些東西,一般就是成員函數(對于清教徒來說,不是一般,而是必然)。假如,函數fn的實現中,就用到Base的幾個成員函數,比如說f1,f2,…,fn。換句話說,雖然fn(Base* bb)表面上要求一定要Base的子孫后代才能擔當重任,但實際上,只要別的class,不必跟Base有半毛錢關系,只要這個class里面支持f1,f2,…,fn這些操作,那么原則上他就有資格到fn里面一游。天下唯有德者居之,不必講究什么貴族。但是,在沒有接口的等級森嚴的封建社會里面,就算你有驚天之地之能,就因為你沒有某種高貴的血統,所以你就不行。
在單根類的王國中,所有對象都源于Object,也可以通過反射,通過函數名字運行時獲取串f1,f2,fn等成員函數,然后再人肉編譯器關于參數信息和返回值類型,以擺脫Base的類型桎梏,但是,估計也只有在最特殊的時候,才會這樣玩。這樣玩,簡直置編譯器的類型檢查于不顧,靜態語言就是要盡可能的挖掘編譯器類型檢查的最后一絲潛力。
接口的出現,就在縱向的類型關系上撕開一道道口子,從而盡最大限度釋放對象的能力。時代不同了,現在接口IBase里面聲明f1,f2,fn等函數,然后函數fn的入參為IBase,也即是 int fn(IBase* bb),以明確表示fn里面只用到IBase的函數,語義的要求上更加精準。然后,任何class,只要其實現了接口IBase,就有資格被fn接納,不必再是Base之后了。所以說,要面向接口編程,就是要面向功能來搬磚,選擇的樣本空間就廣闊了很多。接口是比具體類型要靈活,但不意味著所有的地方就必須只出現接口,class類型就沒用了,當然不是,有些地方就很有必要用具體類型,比如說string類型,比如說復數這些,就必須明確規定具體類型,無須用到接口的靈活性。總之,還是那句話,沒有銀彈,具體問題具體分析。
使用對象,其實就是在使用對象的成員函數,那么,接口也可以看成是成員函數的粒度管理工具。所以,接口就表示了一批成員函數,需要用一批成員函數的時候,用接口最為方便。坊間有一些犯virtual恐懼癥的c++猿猴,高高興興地用一批function代替接口,罔顧其性能(時間空間)的損失、使用上的不便,哎!面向對象是強有力的抽象工具,比之于面向過程,函數式,有著獨特的優點,反正代碼構架上,優先使用面向對象,絕不會錯。而面向對象,就必然回避不了接口。
坊間支持面向對象語言中對接口的支持,當以rust,scala的trait機制最為令人喜歡,非侵入式啊,自然狗語言的也還好,但是,本人最反感,反正,狗語言上一切獨有特性,本人都本能地毫無理由排斥。自然,java、C#或者c++的多繼承,最為笨拙,呆板。
java、C#里面,類能夠實現的接口,在類的定義中,就已經定下來了。類一旦定義完畢,與該類相關的接口就定下來,鐵板一塊,密不透風,不能增不能減也不能改。你明明看到一個類就已經實現了某個接口的所有方法(函數名字和簽名一模一樣),但就是因為該類沒有在定義中明確說明實現該接口,所以編譯器就死活不承認該類實現這個接口。只能用適配器模式,也即是新造一個class,實現該接口,包含舊類的對象,將接口的所有方法都委托給對象的相應函數來做。java的繁文縟節就是這樣來的,規規矩矩,畢恭畢敬,一步一個腳印。更麻煩的是,每次傳遞參數都要new一個適配器對象來滿足參數的要求,這是最讓人難受的地方。
java、C#的這種接口機制,實在與現實對不上號,真是找不到任何原型,任何類型的物品,就算是新造的東西,我們都不可能一開始就窮盡它的所有性質所有功能。就算是藥物,都有可能是歪打正著的功能,比如偉哥的功能,是其研發階段中意想不到的。java、c#的這種接口,會很干擾類的完整最小化的設計原則,進而加大類的設計難度。當然,它也非一無是處,起碼,類支持多少接口,一眼就看出來了,毫無疑義。問題是,接口這種東西,本質上就應該是不確定的橫向視角來考察類的關系。java、C#下的接口問題,大大限制了接口的使用場合。
其次,繼承時,子類就繼承了基類的所有東西,包括其實現的接口。但是,有些時候,子類并不想擁有父類的某些接口。比如,鴨子應該算是鳥類的一個子類,而鳥類支持“會飛”這個接口,但是鴨子顯然不會飛,也就是說,雖然鴨子包含了鳥類的所有數據,但是它不擁有會飛這個功能。對此,我們希望在編譯期間,就能在要求會飛的場合下,傳鴨子對象進去時,編譯器報錯。但是,對此,只能在運行中報錯,而且,還是在調用會飛的成員函數里面才報錯。原則上,編譯器是可以知道鴨子不會飛這個概念的,但是,由于java、C#的接口控制粒度單一,滿足不了這種要求。
再次,接口不能組合,比如說,函數fn的參數,假設名字為pp,pp要求同時實現接口IA,IB。對此,java、C#中是沒有語法滿足這種多個接口的要求。遇到這種需求時,只能用強制類型轉換,先隨便讓參數類型為IA或者IB,然后在必要時,強制轉換為另外的類型,只能在運行時報錯。又或者是,新造一個接口IAB從IA,IB上繼承,然后函數fn的參數pp的類型為IAB,但是這樣,依然存在不足,假如某個類實現IA和IB,但是沒有表明它實現IAB,那么還是不能滿足參數的要求。接口組合的問題,不管是go、rust,都沒有很好的支持,只能到運行時類型轉換才能發生。
最重要的是,這種接口機制違反了零懲罰的機制。就以c++為例來說明,就只論接口好了,也即是只有虛函數但是沒有成員字段的基類。為了方便描述,還是舉例子。
struct IA {virtual void fa() = 0;};
struct IB {virtual void fb() = 0;};
struct Base{…};
struct Derived : public Base, public IA, public IB{…};
接口IA有虛函數,里面就要有一個指針指向其虛函數表,所以其內存占用就是一個指針的大小;同理,IB也如此。表面的意思是Derived實現了接口IA,IB,實際上,在C++中,接口實現就是繼承,也就是說每個Derived的實例都要包含IA,IB里面的數據,指向對應虛函數表的指針字段,也即是有兩個指針。這里做不到零懲罰的意思,是說, Derived為了表明自己有IA、IB的能力,每個對象付出了兩個多余的內存指針空間的代價,即便是對象不需要在IA、IB的環境下使用,這個代價都避免不了。零懲罰抽象,就是要用到的時候才付出代價,哪怕這個代價可以大一點。用不到時,則不必消耗哪怕一點點空間時間上的浪費。空間上浪費的問題不在于節省內存,而在于喪失了精致的內存布局,進而影響到二進制的復用。這一點,非侵入式接口就不用也沒辦法在對象身上包含其所支持的所有接口的虛函數表指針,因為類型定義完畢,后面還可能在其上添加新的接口實現。
而由這幾點問題引申出來的其他缺陷就不必提了。反正,C++,包括java,C#的這種接口機制最不討人喜歡了。
至于狗語言的鴨子接口,有時會出現函數名字沖突的小問題,稍微改一下名字就好了。主要是這種接口機制只要一個類包含了某個接口的所有成員函數,就隱式認為它實現了這個接口。這里會有暗示(誤導,誘惑),就是定義類的成員函數時,會有意或者無意地遷就現有接口的成員函數,同樣,聲明接口成員函數時,也會有意無意地往現有類的成員函數上靠。從而導致真正函數的語義上把控不夠精準。并且,這種機制太過粗暴,萬一這個類雖然支持某個接口的所有函數,但是并不一定就意味著它就要實現這個接口了。狗語言最令人反感之處就是各種自作聰明自以為是的規定。當然,由于狗語言的成員函數可以非侵入式,這個問題造成的不便一定程度上有所減輕,但是,說實在,就連非侵入式的成員函數,本座也不太喜歡了。另外,僅僅從語言層面上,不借助文檔,很難知道一個類到底實現那些接口,某個接口被那些類實現,java、C#的接口在這一點的表現上就很卓越。其實,本座反感狗語言的最大原因還是因為狗粉,相比之下,java粉、php粉等粉,就可愛多了。
rust以trait形式實提供的接口機制就不多說了,語法形式上簡潔漂亮,基本上夢寐以求的接口樣子就是這樣子的了。
以上語言的接口,全部屬于靜態接口,也即是類型所實現的接口在編譯期間就全部定下來了,運行時就不再有任何變化。但是,如果對象一直在變化,好比生物,就說人類好了,有嬰兒少年青年中年老年死亡這些變化階段,顯然每一階段的行為能力都大不一樣,也擁有不同頭銜,不同身份。也就是說,現實中,活生生對象的接口集合并非一成不變,它完全可以現在就不支持某個接口,高興時候又可以支持了,不高興時就又不支持了,聾了就聽不到聲音,盲了就看不見,好似消息發送那樣子,顯然以上語言是不支持這種動態需求的接口的。
另外,com的接口查詢雖然發生在運行時,但是,com的規范,比如對稱性、傳遞性、時間無關性等規則,硬是把com從動態接口降維到靜態接口,這也可以理解,因為動態接口的應用場景真的并不多。這些都沒什么,com最根本的問題,還是在于接口要承載類的功能,當然,這樣也有好處,比如語言的無關性。IUnknown的三大成員函數分明就是類的本職工作,AddRef,Release管理對象的生命周期,Query查詢所要的接口。生命周期由對象粒度細化為接口粒度,就顯得太瑣碎,要謹記好幾條規則,要小心翼翼地應付AddRef,Release的函數調用,智能指針也只能減輕部分工作量,這就是粒度過小帶來的痛苦。而Query的本質就是對象所實現接口集合,這是對象的本分工作,現在搞成接口與接口之間的關系。由于接口越俎代庖,承接了類的職責,就要求每個接口都要繼承IUnknown,本來接口之間就應該沒什么關聯性的才對,還導致com的實現以及使用,在c++下,非常繁復麻煩,令人頭皮發麻。所以說,類與接口,一體兩面,誰也不能代替誰。
---------------------------------------------------------------------------------------------------------------------------------
備注:現實世界中,一種或幾種功能就能推導出來其他性質,對應到接口中,就是如果對象實現某些接口,就表示它能實現另外其他接口。目前的語言,也就是接口繼承,子接口繼承父接口,那么,如果一個類實現了子接口,就表示它也實現了父接口,語言明面上只支持這種接口的蘊含關系。對于其他的蘊含情況,只能用適配器來湊數,而在非侵入式接口中,其語言形式就顯得更加的累贅,這一點,在java上尤為突出。其實,說到底,適配器模式只是彌補語言不支持接口蘊含機制的產物。
2017年7月14日
類的設計在于用恰到好處的信息來完整表達一個職責清晰的概念,恰到好處的意思是不多也不少,少了,就概念就不完整;多了,就顯得冗余,累贅,當然特例下,允許少許的重復,但是,這里必須要有很好的理由。冗余往往就意味著包含了過多的信息,概念的表達不夠精準,好比goto,指針,多繼承這些貨色,就是因為其過多的內涵,才要嚴格限制其使用。好像,more effective c++上說的,class的成員函數,應該是在完整的情況下保持最小化。但是,這里我們的出發點,是成員數據的完整最小化。
最小化的好處是可以保持概念最大的獨立性,也意味著,可以用最小的代價來實現這個概念,也意味著對應用層的代碼要求越少,非侵入式?好比c++11 noexcept取代throw(),好比從多繼承中分化出來接口的概念,好比不考慮多繼承虛繼承的普通成員函數指針。又比如,如果不要求只讀字符串以0結束,那么就可以把只讀字符串的任何一部分都當成是只讀字符串。類的對外功能固然重要,但是,類不能做的事情,也很重要。
首先是要有清晰的概念以及這個概念要支持的最基本運算,然后在此基礎上組織數據,務求成員數據的最小化。當然,概念的產生,并非拍著腦袋想出來的,是因為代碼里面出現太多那種相關數據的次數,所以就有必要把這些數據打包起來,抽象成一個概念。好比說,看到stl算法函數參數到處開始結束的迭代器,就有必要把開始結束放在一起。比如說,string_view的出現,這里假設其字符存儲類型為char,string_view就是連續char內存塊的意思,可以這樣表示
struct string_view
{
const char* textBegin;
size_t length; //或者 const char* textEnd
};
這里的重點是,string_view里面的兩個成員字段缺一不可,但是也不必再添加別的什么其他東西。然后,在這兩個數據上展開實現一系列的成員函數,這里,成員函數和成員字段這兩者,有一點點雞生蛋生雞的糾結,因為必要成員函數的集合(原始概念的細化),成員函數決定了成員字段的表示,而成員字段定下來之后,這反過來又能夠驗證成員函數的必要性。不管怎么說都好,成員函數的設計,也必須遵從最小完整化的原則。再具體一點,就是說但凡一個成員函數可以通過其他成員函數來實現,就意味著這個函數應該趕出類外,作為全局函數存在。當然,這也不是死板的教條,有些很特殊的函數,也可以是成員函數,因為成員函數的使用,確實很方便。
可能會有疑惑,感覺所有的成員函數其實都可以是全局函數。或者說,我們可以對每一個成員字段都搞一對set、get的函數,那么所有的其他成員函數就可以是全局函數的形式,很容易就可以遵守最小完整化的原則。當然,這是明顯偷懶,拒絕思考的惡劣行為。與其這樣,還不如就開放所有的成員字段,那樣子就落入c語言的套路了。所以的法論是,一個函數,這里假設是全局函數,如果它的實現必須要訪問到成員字段,不能通過調用該類的成員函數(一般不是get,set)來達到目的,或者,也可以強行用其他函數來完成任務,但是很麻煩,或者要付出時間空間上的代價,那么就意味著這個函數應該是該類的成員函數。
類的設計,就是必不可少的成員字段和必不可少的成員函數,它們一起,實現了對類的原始概念的完整表達,其他什么的,都不必理會。一個類如果不好寫,往往意味著這個類的功能不專一,或者其概念不完整,這時,可以不要急著抽象,如果一個類有必要誕生,那么在代碼的編寫中,該類的抽象概念將一再重復出現,猿猴對它的理解也越來越清晰,從而,水到渠成地把它造出來。所有非需求推動,非代碼推動的,拍著腦袋,想當然的造類行為,都是在臆造抽象,脫離實際生活的藝術,最終將被淘汰。
類的設計,其著眼點在于用必要的數據來完整表達一個清晰的概念。而繼承,則是對類的概念進行細化,也就是分類,好比說生物下面開出來動物、植物這兩個子類,就是把生物分成動物、植物這兩類,繼承與日常生活的分類不太一樣,繼承的分類方式是開放式,根據需要,隨時可以添加新的子類別。整個類的體系,是一顆嚴格的單根樹,任何類只能有一個根類。從任何類開始,只能有一條路徑回溯到最開始的根類,java、C#中就是Object,所有的類都派生自Object,這是一棵大樹。單根系下,萬物皆是對象,這自然很方便,起碼,這就從語言層面上直接支持c++ std的垃圾any了。而由于java、C#完善的反射信息,拋棄靜態類型信息,也可以做動態語言層面上的事情,而c,c++的void*,所有的動態類型信息全部都在猿猴的大腦中。java平臺上生存著大把的動態語言,而且,性能都還很不錯。
相對很多語言來說,c++就是怪胎就是異數,自有其自身的設計哲學,它是多根系的,它不可能也沒必要搞成單根系,當然,我們可以假設一個空類,然后所有的類都默認繼承自這個空類。c++的所有類組成一個森林,森林里的樹都長自大地。但是不管怎么說都好,只能允許單繼承,千萬不要有多繼承,這是底線,千萬千萬不能違背(當然,奇技淫巧的場合,就不必遵守這個戒條,多繼承千般不是,但是不可或缺,因為它可以玩出來很多花樣,并且都很實用很必要)。最起碼,單根系出來的內存布局直觀可預測,一定程度上跨編譯器,只有良好的內存布局,才有望良好的二進制復用。另外,父類對子類一無所知,不要引用到子類一丁點的信息,要保持這種信息的單向流動性。
在這種單根系的等級分明的階級體系下,一切死氣沉沉,沒有一點點的社會活力。顯然,只有同屬于同一父類的類別之間,才能共享那么一丁點可憐的共性。如果沒有接口搗亂,將是怎樣的悲劇,最好的例子,mfc,真是厲害,沒有用到接口,居然可以做出來嚴謹滿足大多數需要的gui框架,沒有接口,并不表示它不需要,因為mfc開了后門,用上了更厲害的玩意----消息發送,即便如此,mfc有些地方的基類還有依賴到子類,這就很讓人無語了。
c++下,類的設計絕對不對兒戲,一定要清楚自己想要的是什么,抽象出來的概念才不會變成垃圾。大而全的類,遠遠不如幾個小而專的細類。java,C#下的類開發很方便,但是粒度過大,把一攬子的東西都丟給你,強賣強買,反正只要類一定義,必然相應的就會出現一大坨完善的反射信息,而對象里面也包含了一些無關緊要的成員字段,而對象的訪問,也全部都是間接引用的訪問,雖然,現在計算機性能過剩,這些都無傷大雅。c++給了開發者最大的選擇,而搞c++的猿猴,基本上都智力過剩,對于每種選擇,都清楚其背后的代價以及所要到達的目的,所以雖然開發時候,存在心智包袱影響開發效率,但是,但內心就不會存在什么性能包袱的負罪感。就個人而言,還是喜歡c++這種最高自由度的語言,有時候,對于內存最細致的控制,可以得到更精簡的設計,這里無關運行性能,好比說,在c++中,只要內存布局一致,即便是不同類型的對象,通過強制類型轉換來統一對待,進而做匪夷所思之事,好比COM里面,為了聚合復用,一個類,竟然可以針對同一個接口提供兩套實現方式。這種方便,在其他強類型語言中是不支持的。
某種意義上講,c++在面向對象上提供的語言機制,就是為了方便地生成各種內存布局,以及此內存布局上所能支持的操作,虛函數用以生成一堆成員函數指針,繼承則用以方便地生成一坨成員字段,……。所以,c++的面向對象就是面向內存布局地設計,而多繼承、虛繼承、模板這些鬼東西很容易就導致內存布局的失控,不過,如果使用得當,卻又有鬼斧神工之奇效,創造出來其他語言所沒有的奇跡。真的,論動態行為藝術,任何語言在c++這個大人面前都是幼兒園里的小學生。
為了引出接口,本座花大力氣做科普。這也沒辦法,因為類雖然是基礎,但是靜態面向對象的精華,全部都在接口上。只有清晰明確類的功能職責,才能理解接口的必要性以及其多樣性。那么,可不可以只有接口,沒有類的。可以,就好像com那樣子,而代價是,使用起來,各種不方便。這個世界,從來就不存在包治百病之萬能藥。什么事情都能做的意思就是什么都做不好。
2017年7月12日
此文只是雜亂的記錄一點點對于面向對象的個人看法,有些觀點也并非原創。沒什么系統性可言,雖然筆者稍作整理,但始終還是顯得很散亂,只是一些片段的堆積。
由于涉及的題目過于龐大,反而不知道如何下筆。先羅列一下問題,之間沒有嚴格的先后之分,純粹就是筆者想到哪里,就寫到哪里。也不一定就會解答。繼承的本質是什么?為什么一定要有接口?c++多繼承為何飽受非議,真的就一無是處?為何筆者就反感go接口,反正go獨有的一切,筆者都是下意識的排斥?功能繁雜的Com,結合C++的自身特點,能否改頭換面? ……
在原教旨眼里,面向對象的教義就是“對象+消息發送”,整個程序由對象組成,而對象之間的就僅僅只通過發送消息響應消息來交互,程序的功能都是在對象與對象的來回消息發送中完成,用現實事情類比,人類就是一個個活生生的對象,人類通過消息的往來,比如語音、文字、廣播等,有人制造新聞,有人接受到這些消息后,各自反應,最后完成一切社會活動。好像說得有點抽象,展開來說,其實就是,消息的發送者,原則上不需要事先了解目標對象的任何背景資料,甚至他明知道對方不鳥消息,比如說,明明對方就是一個乞丐,但是并不妨礙你向他借500萬人民幣,反正,消息就是這樣發送出去的。然后,對象接受到消息之后,就各自反應,比如說有人真的借錢給你;有人哭窮;有人嘀咕你到處借錢,無恥;……,各式各樣,不一而足。
聽起來好像人類社會活動就是消息的往來下推動,艱難的前進,但是,這能拿來搬磚嗎?可以的,真的可以!即便是C語言,都可以來搞消息發送這種高大上的事情,就好像win32那樣子,通過SendMessage函數給窗口發送消息,其簽名如下:
LRESULT SendMessage(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);
好像參數有點多。說白了,消息發送就相當于成員函數函數調用的一個新馬甲,換了一種說法而已。成員函數調用,形式是這樣子,obj.fn(param1, param2, …),涉及到對象,函數名字,還有參數,可能參數數量不止一個,參數類型也各不一樣,這些都沒關系。hWnd為窗口,也即是對象;Msg為函數名稱,現在用正整型編號來代表,有些消息發送系統用原子,qt好像是用字符串(性能堪憂啊);wParam,lParam可以看成void*類型,也即是函數的參數,用這兩個值封裝所有的參數。天真,天下函數參數類型成千上萬,參數數目或0個、或1個、或三五個、或七八個,就wParam,lParam這兩個弱雞,就能封裝得過來?可以的,通過強制類型轉換,就可以讓void*的值保存char、int、float等值,又或者是將參數打包為結構體,這樣子,就可以應付千千萬萬的函數參數要求,這樣子,不要說,有兩個wParam,lParam來傳遞參數,就算是只有一個,也都可以應付千千萬萬的函數要求。
那么,如何響應消息?可以參考win32的原生api開發,這里就不展開了。原理就是,每個對象都有一個函數指針,那個函數把全部的成員函數都壓縮在一個龐大的switch語句里面,每個消息編號case分支,就代表一個成員函數,顯然,這個分支,要先將wParam,lParam里面在還原成對應參數的實際情況,然后再執行相應操作。
SendMessage顯然抹去了所有窗口的具體類型信息,甭管你是按鈕、漂亮按鈕、菜單、編輯框、……,全部一律都退化成窗口對象。要往編輯框里面添加文字,就給它發送添加文字的消息,wParam,lParam就帶著要添加的文本和長度。而不是調用編輯框的添加文字的成員函數來做這個事情,最明顯的事情,就是也可以給按鈕窗口也發送添加文本的消息,雖然按鈕窗口對此消息的反應是啥也不做。令人驚訝的是,你可以子類化一個按鈕窗口,讓它對添加文本的消息做出反應,這完全是可以的。
顯然,原教旨的面向對象教義,的而且確,靈活,解耦徹底,不同類型對象之間的耦合關系一律不復存在,之間只有消息的往來。隨心所欲的發送消息(胡亂調用成員函數),自由自在的反應消息(一切全無契約可言),不理睬,或者這一刻不理睬下一刻又動了,或者這一刻動了下一刻又拒絕反應。甚至,消息還可以保存,排隊,求反,疊加什么的,也即是消息已經是一種抽象數據類型了,支持多種運算。相比于不知所謂的基于類的靜態面向對象(繼承封裝多態),簡直不可同日而語,太多的約束,呆板的語法,深入的哲學思考,架床疊屋的類型關系,也好意思學人家叫面向對象。
當然,對象+消息發送這種機制,付出的代價也是很巨大的,基本上,函數調用的靜態類型檢查不服存在,所有問題都要到運行時才能發現。并且,消息發送的語法也很不直觀,必須各種類型轉換,而響應消息時又必須轉換回去。此外,為函數定義消息編號,也很惡心。不過,這些在動態語言里面都不是問題,反正,動態語言里面沒有靜態類型約束。另外,筆者用template、全局變量、宏等奇技淫巧,在c++里面,已經實現了類型安全的消息發送框架,比如,Send(obj, kAppendText, U8String(“hello”)),而對象實現對消息的響應,直接也是成員函數的形式,并且還是非侵入式的,也即是說,在main函數之前,可以隨時在任意地方給對象添加新的消息反射,所有參數上類型轉換以及返回值上的類型轉換,全部都不需要了。 但即便是這樣,也不贊成原教旨的面向對象到處泛濫。原因是,用它寫出來的程序,類型層次很不清晰,相比于架構良好的類形式的面向對象程序,可讀性遠遠不如,也不好維護。更深刻的原因是,對象+消息發送的威力太驚人,用途太廣,任何多態上的行為,都可以用它來做。什么都可以做,就意味著什么都盡量不要讓他來做。
其實,即便java、C#這種繼承封裝多態的面向對象千般弱雞各種繁文縟節,也不妨礙人家稱霸天下,到處流行。你對象+消息發送再美妙,流行度都不及人家java一個零頭,obj c還不是靠著ios的流行才有所起色,擠入排行榜十名內。雖然說市場不能說明什么,但是對比如此懸殊,自有其道理。
再說,靜態類型的成員函數調用模式,廣泛存在于人類社會活動中。人與人之間的很多事情,其實只要滿足一定的條件,必然就會發生,其后果也可以預料。很多消息的發送,其實是有考慮到對方的身份問題,才會發起,好比小孩子跟爸媽要零用錢的消息,小孩子再發送要零用錢的消息,一定是針對親人才發起的。真相是,往往要滿足一些必要條件,消息才得以發起,當然,只要你高興,隨時都可以發起任何消息,問題是,這種人多半不正常。量體裁衣,針對什么樣的問題,就應該采用相應的手段,一招鮮吃遍全天下,行不通的。具體問題,必須具體分析。每種問題,都有自己最獨特有效的解法。筆者在原教旨的面向對象上重復太多內容,連自己都惡心,以后應該很少再提及。
所以說,面向對象的設計,首先應該采用的必然還是繼承封裝多態的思路。在此基礎上,根據不同的動態要求,采用不同策略來應對。企圖用萬能的消息發送來代替靜態類型面向對象的荒謬就如同用僵化的面向對象來模擬一切動態行為,兩者都是犯了同樣的毛病。可是,靜態面向對象做設計,又確實困難重重,而最終的開發成果,總是讓人難以滿意。那是因為,廣大勞動群眾對靜態面向對象一些基本概念的理解,存在這樣那樣的誤區,而由于面向對象語言(java,C#)還缺乏一些必要機制,導致設計上出現妥協,原則性的錯誤越積越深,以至于最后崩盤。其實,不要說一般人,就連大人物,在面向對象上,也都只是探索,好比c++之父BS,搞出來多繼承,虛繼承,iostream體系,在錯誤的道路上,越走越遠,越走越遠。
好吧,其實,多繼承,還是很有作用的,在很多奇技淫巧上很有用武之地,很方便。但是,用多繼承做架構的危險,就在于其功能太過強大。這就意味著它要淪落成為goto啊、指針啊那樣的角色,先甭管它鉆石尷尬。多繼承的最重要角色,概念實現,也即是接口,也即是定義一批虛函數,里面沒有任何數據,這個抽象就必須鮮明,這一點,java和C#就做得很到位。就應該從多繼承上提煉出來這么一個好東西,咦,對了,為何要有接口?沒有接口,就真的不行嗎?是的,靜態面向對象里面,接口確實必不可少。
繼承,本質上就是分類學。而分類,最重要一點,就是任何一件元素,必須也只能只屬于其中一個類,不得含糊。可以存在多種分類方式,但是,一旦確定某種分類方式,那么集合里面的一個東西,就必須只能屬于其中一大類。繼承,就是分類的一再細化,也是概念的繼續豐富。比如說,從生物到動物到哺乳動物,概念包含的數據越來越多。所以說,繼承體現的是數據上的豐富關系,它強調的是數據的積累,從遠古基類開始,一路積累下來的數據,全部必不可少,也不得重復,一旦違反這條底線,就意味著繼承體系上的錯亂。繼承,相當于類型的硬件,缺乏硬件元器件時,就無法完整表達該類型的概念。比如說,人類可分為男人、女人,自然,男人有男人的陽剛,女人有女人的陰柔,那么陰陽同體怎么辦,集兩性之所長,難道就要陰陽人多繼承與男人女人嗎?那么,這樣繼承下來,陰陽人豈不是就是有兩個頭,四只手,四條腿了,啊,這不是陰陽人,這是超人,抑或是怪物。所以,陰陽人應該是人里面的一個分支,也即是,人的分類,就要有男人、女人、陰陽人這三大基類。再次強調,繼承是為了繼承數據,而不是為了功能,功能只不過是數據的附帶品。那么,怎么描述男人的陽剛、女人的陰柔,怎么避免陰陽人引入后,分別從男人陽剛,女人陰柔上復制代碼呢?此外,再次考慮平行四邊形,下面好像又有菱形,有矩形兩大類,然后身集菱形矩形的正方形,這里的分類該如何處理,難道忍不住要讓正方形多繼承菱形矩形嗎?從這個意義上講,在同一體系下,多繼承的出現,理所當然,大錯特錯,由此可知,iostream就是敗類。iostream通過虛繼承避免絕世鉆石的出現,但是這個虛繼承啊,真是要讓人呵呵。C++中引入虛繼承真是,怎么說呢,好吧,也算腦洞大開的優良物品,也不是完全一無是處,起碼,在iostream上就大派用場了。你就說說,虛繼承那點不好了?就一點,為了子子類的千秋基業,子類必須虛繼承基類,子類受子子類影響,就這一點,你能忍。
突然發現,文章已經很長了,不管了,這就打住。至于非侵入式接口,以后再說吧!
2017年7月11日
C++的面向對象設計能力,與java,C#這兩個雜碎相比,一直都是一個大笑話,現在誰敢正兒八經地用c++搞面向對象的框架系統,業界都用java、C#搞設計模式,那關C++什么事情了。而C++也很有自知之明,很知趣,98年之后,就不怎么對外宣稱自己是面向對象的語言,就不怎么搞面向對象研究了(難道是c++下的面向對象已經被研究透徹?),一直在吃template的老本,一直到現在,template這筆豐厚的遺產,貌似還夠c++吃上幾十年。今時今日,virtual早就淪落為template的附庸,除了幫助template搞點類型擦除的行為藝術之外,就很難再見到其身影了。有那么幾年,業界反思c++的面向對象范式,批斗virtual,特別是function出現之后,要搞動態行為,就更加不關virtual的什么事情了。而那幾年,本座也學著大神忌諱virtual關鍵字。現在大家似乎已經達成共識,c++里頭的面向對象能力很不完善,要玩面向對象就應該找其他語言,比如java、C#雜碎;或者更動態類型的語言,好比python,Ruby;或者干脆就是原教旨的面向對象(消息發送),object C,smalltalk。
是啊,1、沒有垃圾回收;2、沒有原生支持的完善反射能力;3、多繼承、虛繼承導致的復雜內存布局。這三座大山面前,c++的碼猿哪敢染指什么面向對象,只在迫不得已的情況下,小心翼翼地使用virtual。但是,事實上,要玩面向對象,c++原來也可以玩得很炫,甚至,可以說,關于面向對象的能力,c++是最強的(沒有之一)。這怎么可能?
所謂的面向對象,說白了,就是對動態行為的信息支持,能在面向對象設計上獨領風騷的語言,都是有著完善的運行時類型信息,就連lisp,其運行時元數據也都很完備。靜態強類型語言(java、C#)與動態語言比,顯然有著強大的靜態類型能力(這不是廢話嗎),能在編譯期就提前發現類型上的諸多錯誤,但是也因此帶上靜態約束,導致呆板、繁瑣的代碼,java的繁文縟節,就是最好證明;而動態語言恰好相反,代碼簡潔,廢話少,但是喪失靜態信息,所謂重構火葬場,那都是血和淚的教訓。靜態語言與動態語言真是一對冤家,如同光的波粒性,己之所長恰是彼之所短,己之所短又是彼之所長,魚與熊掌不可兼得。而C++竟然能集兩家之所長,在靜態語言的領域中玩各種動態行為藝術,比如動態修改類型的反射信息,千奇百怪的花樣作死(喪心病狂的類型轉換);在動態范疇里面,又可以在編譯期榨取出來靜態類型信息,比如,消息發送的參數信息,想想win32的無類型的wparam和lparam,每次都要猿猴對照手冊解碼,從而最大限度地挖掘編譯器的最大潛力。所以說,c++是最強大的面向對象語言,沒有之一。而把靜態和動態融為一體之后,c++的抽象能力也到達一個全新的高度,自動代碼生成,以后再發揮,這是一個龐大的課題。C++令人發指的強大,絕對遠遠超乎等閑猿猴的想象,特別是那批c with class的草覆蟲原始生物。C++只在部分函數領域的概念上表現令人不滿,比如lambda表達式的參數類型自動推導,monad表達式,缺乏原生的延遲求值等。當然,c++整個的設計理念非常繁雜隨心所欲,但是,卻可以在這一塊混沌里面整理出來一些舉世無雙的思想體系,就是說,c++是一大堆原材料,還有很多廚房用具,包括柴火,讓猿猴自行下廚,做出來的菜肴可以很難吃,也可以是滿漢全席,全看猿猴的手藝。
當然,要在c++里頭搞面向對象,多繼承,虛繼承的那一套,必須徹底拋棄。最大的問題是,多繼承會導致混亂未知的二進制內存布局,虛函數表也一塌糊涂,十幾年前,c++設計新思維的基于policy的范式,雖然令人耳目一新,也因為這種范式下對象的內存布局千奇百怪,所以,即便是最輕微的流行也沒有出現過。當然,也不可能大規模搞消息發送這種很geek的套路,功能太泛化了,其實,消息發送就是動態的給對象添加成員函數,并且可以在運行時知道對象有多少成員函數,那個成員函數可以對該消息做出反應,消息可以是字符串,整型ID(原子), MFC的消息映射表(BEGIN_MESSAGE_MAP,…)就是一個功能嚴重縮水版的好例子,c++下支持消息映射的庫,絕對可以比破mfc的那一套要好上千百倍,不管是性能、類型安全、使用方便上。目前除了在gui這種變態的場合下才需要大搞消息發送,其他場景,完全可以說用不上,雖然說消息發送很強大很靈活,但也因為其殺傷力太厲害,反而要更加慎重。這好比goto,好比指針,好比stl的迭代器,什么都能做的意思,就是什么都盡量不讓它做。
那么,c++下搞面向對象,還有什么法寶可用呢?當然,在此之前,我們先要直面內存分配。內存既是c++的安身立命之本,又是c++淪落為落水狗喪家犬之幕后大黑手。假如不能為所欲為的操作內存,那么c++的折騰法子,奇技淫巧,起碼要死掉一大半以上。而由于要支持各種花樣作死的內存操作,c++的垃圾回收遲遲未曾出現,就連以巨硬之大能整出來的.net那么好的gc,霸王硬上弓,在給原生c++強硬加上托管功能(垃圾回收),都出力不討好。可見未來垃圾回收,對c++來說,嗯,想想就好了。內存是資源,沒錯,用raii來管理,也無可厚非。但是,內存卻是一種很特殊的資源,1、內存時對象的安身立命之所;2、不同于普通資源,內存很多,不需要馬上用完就急急忙忙啟動清理工作,只要系統還有大把空余的內存,就算還有很多被浪費了的內存,都不要緊,gc也是因為這個原因才得以存在。相比內存,普通資源給人的感覺就是數量及其有限,然后要提交工作結果,否則之前所做努力就廢了。所以,對于內存,應該也要特別對待。就算raii,也要采用專門的raii 。
假設我們的程序里面使用多種內存分配器,比如說,每個線程都有自己專有的內存allocator對象,然后,線程之間的共享數據由全局的內存分配器分配,線程的內部對象都用線程的專屬allocator來分配,那么,內存分配器就是一個線程局部變量(tls,thread local storage)。于是,可以規定,所有的內存分配都通過GetTlsAllocator()來new對象,當然,確定是全局共享變量的話,沒辦法,就只能用GetGlobalAllocator()來new對象。那么,有理由相信,啟動一個任務時,我們先定義一個arena allocator變量,并令其成為當前線程的專屬內存分配器,那么這個任務后面的所有new 出來的對象,包括循環引用,都不必關心。只要任務一結束,這個arena allocator變量一釋放,所有寄生在它身上的對象,全部也都消失得干干凈凈,沒有任何一點點的內存泄露。就算任務內部有大量的內存泄露,那又如何,任務一結束,所有跟此任務有關的一切內存,全部成塊清空。總之,不要以常規raii來解決內存困境,解放思想,在內存釋放上,我們可以有九種辦法讓它死,而不是僅僅靠shared_ptr,unique_ptr,weak_ptr這些狹隘的思維。
其次,完善的面向對象設計,避免不了完備的反射,用以在運行時提供動態類型信息,無參模板函數可以把靜態類型映射成全局唯一變量,好比,TypeOf<vector<int>>,返回vector<int>的全局唯一的const TypeInfo*對象,這個對象包含了vector<int>的所有靜態類型信息,可以這么說,在靜態類型層面上vector<int>所能做的任何事情,比如定義一個vector<int>的變量,也即是創建對象;遍歷、添加元素、析構、復制賦值、元素數量等等一切操作,與vector<int>對應的TypeInfo對象,統統都可以做到。所不同的是,vector<int>的靜態類型代碼,只能用于vector<int>自身的情況(這樣子可放在源文件中),又或者是通過template,表現行為類似于vector<int>的數據類型(代碼必須在頭文件上)。而用TypeInfo*做的事情,全部都在運行時發生,所有的靜態類型信息,全部被帶到運行時來做,所以這些代碼全部都可以處在源文件里面,甚至動態庫里頭,只不過是TypeInfo*操作的對象是一個二進制內存布局和vector<int>一模一樣的內存塊,可以通過強制類型轉換,把運行時的內存塊轉換成靜態編譯時的vector<int>。其實這里的思想,就是想方設法將豐富多彩的靜態類型信息無損的保存到運行時中,讓編譯時能做的事情,運行時也可以做。差別在于,一個是用靜態類型信息來做事情,這里,任何一點點類型上的錯誤,都會讓編譯器很不高興;一個則是用動態類型信息來做事情,這里,顯然只能讓猿猴人肉編譯器。這里,可見動態類型信息和靜態類型信息的表達能力是等價的,也即是同等重要性的意義,而靜態類型信息的意義有多大,相信大家都知道。
那么,如何建立完備的反射信息,這個必須只能用宏來配合完成,外部工具生成的反射信息代碼,功能很不完備,另外,c#、java等的反射信息全部都是編譯器生成的,可定制性很差。我們需要的是一點都不遜色于靜態行為的動態行為。所以,只有由自己自行管理反射,才能做到真正意義上的完備反射。必要時,我們還可以在運行時修改反射信息,從而動態地增刪對象的行為方式,改變對象的面貌。看到這里,是否覺得很多的設計模式,在這里會有更清晰更簡潔的表達方式呢,甚至,輕而易舉就可以出現新的設計模式。比如,以下定義對象反射信息的代碼。
在c++下,由于全局變量生命周期的隨意性(構造函數調用順序不確定,析構順序也不確定),大家都很忌諱其使用,雖然全局變量功能很強大,很多時候都避免不了。但是,標準上還是規定了全局變量的順序,所有的全局變量必須在main函數之前構造完成,其析構函數也只能在main函數結束后才調用。另外,函數的靜態變量必須在其第一次訪問之前構造完整。基于這兩點,我們就可以在main函數之前構建全部的反射信息,流程是這樣子,所有的類型的反射對象都是以函數內部的靜態指針變量存在,他們都通過調用GetStaticAllocator()的內存分配器來創建,這樣子,提供反射信息的函數,就避免了其內部TypeInfo對象的析構發生。最后,main結束后,由GetStaticAllocator()函數內的內存分配器的析構函數統一釋放所有反射信息占用的內存。最后,附上一個例子
struct Student
{
//ClassCat表示為Student的基類,為空類,所以Student可以繼承它,但是代碼上又不需要明確繼承它,非侵入式的基類。
//ClassCat提供二進制序列化操作,xml序列化,json序列化,數據庫序列化等操作
PPInlineClassTI(ClassCat, Student, ti)
{
PPReflAField(ti, name);
PPReflAField(ti, age);
PPReflAField(ti, sex, { kAttrXmlIgnore }); //表示不參與xml的序列化操作
}
AString name;
int age;
bool sex;
};
struct Config : Student
{
PPInlineClassTI(Student, Config, ti)
{
PPReflAField(ti, map);
}
HashMap<U8String, int> map;
};
下期的主角是非侵入式接口,徹底替換c++上的多繼承,功能遠遠好過C#、java雜碎的弱雞接口,更超越狗語言的不知所謂的非侵入式接口。如果僅僅是完備的反射信息,而缺乏非侵入式接口,在c++下搞面向對象,其實還是很痛苦的。但是,有了非侵入式接口之后,一切豁然開朗。甚至可以說,感覺c++里面搞那么多玩意,都不過是為了給非侵入式接口造勢。然而非侵入式接口一直未曾正式誕生過。
2017年7月10日
古龍說過,一個人的最大優點往往將是其致命的弱點。這句話用在stl的迭代器上,最是合適不過。stl通過迭代器來解耦容器與算法,可謂擊節贊嘆;但是,讓迭代器滿世界的到處亂跑,未免就大煞風景。此話怎講?
其實,有些語言就沒有迭代器的概念,并且還活得很優雅,好比haskell的list啊、tree啊,壓根就不需要什么迭代器,只需要模式匹配,體現其數據結構的遞歸特點,就可以很優雅地表達算法。就是java、c#、C++這幾個破面向對象語言,才需要大用特用迭代器,沒有迭代器就活不下去了。迭代器的出現就是為了彌補其語言喪失清晰表達遞歸數據結構的能力。看到haskell的list到c++的stl下的對應樣子,很多人都表示很難過,因為stl里面,list根本就沒有tail函數,更逞論支持list的tail還是一個list這樣絕妙的idea。一切必須通過迭代器這個萬金油來糊弄其尷尬的困境。
隨便來看看幾行stl算法函數的代碼
Vector<int> nums = {..};
find(nums.begin(), nums.end(), 2);
remove_if(nums.begin(), nums.end(), _1 >= 0); //為了省事,用了bll的風格,在c++11中,要從零開始造一個bll風格的輪子,不能更方便,大概也就兩三百行的代碼
看到沒有,你信不信,隨便統計一下,一打的algorithm函數,起碼就有12個函數的調用之道,必須傳遞container.begin(),container.end()。begin和end這對兄弟,總是成雙成對的出現,說明了一件事情,就是從一開始,它們必須被打包在一起,而不應該硬生生地將它們拆開。知道這一拆開,帶來多少問題嗎?代碼上的累贅還算是小事,比如,簡潔清晰流暢的find(nums, 2),卻要生硬的寫成find(nums.begin(), nums.end(), 2)。當然,這種api設計,也并非一無是處,起碼,在表達容器里面的部分區間時,很方便,好比下面的代碼
int nums[10] = {…};
find(nums+1, end(nums)-1, 2);
看起來,好像的確挺方便的,將begin、end放在一起,要表達這樣的概念,似乎就有些麻煩,但其實,這是假象,當角度變換時,我們可以會有更方便的方式來表達這樣的需求。最起碼,容器的部分區間也應該是由容器本身來表達,而不應轉嫁給迭代器來應付,數組的部分也是數組,樹的分支也是樹,這樣的概念,就應該由容器本身來定義。像是哈希表就不支持部分區間的概念。
為何algorithm的算法,全部(不是基本)都要求一對迭代器。那是因為這些算法的輸入對象,本來就是一個數據集合。而一個迭代器無法完整地表達一個容器,起碼必須一對迭代器才能完整地表達一個數據集。但是,用一對迭代器來作為入參,和用一個區間作為入參,它所體現抽象的側重點完全不同,而由于此種不同,最后的演變結果,也是天淵之別,即是一對迭代器設計思路是淵,自然,而區間的設計方案,顯然是天。
再次回顧上文的結尾,find,find_if,remove, remove_copy, remove_copy_if, remove_if,……,有沒有感受,一股濃濃的過程式風格,十分的笨重,明顯的非正交,濃烈的c語言風格。對于這樣的api,讓本座對委員會的那幫老不死,徹底的絕望了。他們(它們)的審美觀,停留在很低很低的層次上。
將begin,end拆分開來的最大問題,其實也就只是,前一個函數的處理結果,不能平滑的傳遞到下一個函數里面去。比如說,現在函數make_nums返回vector<int>,試比較一下,高下立判。
auto nums = make_nums();
find(nums.begin(), nums.end(), 2); //一對迭代器作為入參
find(make_nums(), 2);//直接數據區間作為入參
說了這么多,我們強烈要求的僅僅是函數風格的api,正交式的函數設計,前一個函數的處理結果可以平滑地傳遞給下一個函數。總結algorithm的一坨函數,本質上只需filter,fold,map,insert(copy)這屈指可數的幾個函數就可以自由地組合出來,并且還能組合出來algorithm上沒有的效果。首先,這幾個函數的返回結果都是數據區的數據對象(里面有begin和end的成員函數,用以返回迭代器)。其次,就是在迭代器上面做文章,以支持filter、map等操作,也就是在*、++、!=這幾個運算符上做花樣,要達到filter、map的效果,很容易的。至于像是要求隨機訪問迭代器概念的函數,太常用的就做到array_view里面好了,或者就明確規定入參就是array_view。
然后stl里面還臆造了一種好像叫做insert_iterator迭代器類型的適配器,用以通過迭代器的語法往容器里頭插入數據,好像很玄妙,實則就是強行拔高迭代器的用途,完全就違背了迭代器出現的初衷。這種扭曲的想法,完全就是上面那一坨病態api的產物。所以,原本的api設計,算法函數必須以容器(數據區間)為入參,內部調用其begin和end成員函數獲得迭代器來遍歷容器的函數,何其清晰的設計思路。但是,stl的設計思路,導致迭代器泛濫,甚至連客戶層面的代碼也大把大把的迭代器,于是迭代器的問題就接二連三的產生,什么失效啊,什么first和last匹對錯誤。還有,導致容器里面的關于迭代器的成員函數多了一倍,哈希表里面也沒有類似于C#里Dictionary的Keys和Values屬性函數,這些用起來很方便的,不是嗎?
stl的這種api設計思路完全不是以方便使用為主,而是以滿足自己的獨特口味為目的。看看find函數,它返回一個迭代器,所以,我們使用時,必須通過用end來判斷要找的東西是否在區間里面,
auto found = find(nums.begin(), nums.end(), 2);
if (found != nums.end()){…}
依本座看,直接就返回指針好了,指針為nullptr,就表示元素找不到,代碼變成這樣
if (auto found = find(nums, 2)){…}
代碼合并成一行,不用再和end比較了。更重要的是,返回結果就是指針,類型非常明確,可以平滑的傳遞到別的函數里;而不是迭代器類型,誰知道迭代器類型是什么類型。template這種東西的類型,能明確下來時,就盡快明確下來。至于說,有些區間的元素不支持返回地址,好比,vector<bool>,很簡單,那就不支持好了。本座編寫c++代碼的原則之一,不求大而全,需求專一,絕不會因為個別同學,就犧牲大多數情況下清晰方便高效的api風格。對于這些異數,必要時,用奇技淫巧解決。你知道,因為多繼承,虛繼承,把成員函數指針這個簡潔的概念搞得非常復雜,不能按正常人方式來使用了,嚴重影響成員函數的用范圍,一直讓本座耿耿于懷。其實,95%以上的情況下,我們就僅僅需要普通成員函數指針而已,另外的5%,也都可以用普通成員函數來封裝。所以,為了彌補這個遺憾,本座做了一個精簡版的delegate,只接受全局函數和普通成員函數,當字段object為空,就表示字段函數指針是全局函數,不為空,就表示函數指針是成員函數。至于其他一切奇奇怪怪的函數,本座的這個delegate就say no,明確拒絕。
stl的這種獨特到處都是,boost更是將其發揚光大,反正設計出來的api,就是不考慮讓你用的舒爽,二進制的布局,更加一塌糊涂。比如,any的使用,是這樣子用的,cout << any_cast<int>(anyValue),這里還好,假如要分別針對any的實際類型來寫代碼,必須這樣子:
if(anyValue.type() == typeid(int))
cout << any_cast<int>(anyValue);
else if (anyValue.type() == typeid(double))
cout << any_cast< double >(anyValue);
…
這種對類型安全無理取鬧的強調,讓人火冒三丈。要本座說,直接在any里面添加Cast模板成員函數,結果就返回指針好了,指針為空,就表示類型不匹配,代碼就變成這樣
if(auto value = anyValue.Cast<int>())
cout << *value;
else if(auto value = anyValue.Cast< double >())
cout << *value;
…
是否就沒那么心煩呢。另外,鑒于stl對于反射的拒絕,采用virtual+template的類型拭擦大法來彌補,其實并不怎么完美。本座用反射重新實現的any,比stl的any好多了,不管是性能、編譯速度、使用方便上,都是要好太多。還有,stl的any,要為每個用到的類型都要生成一個實實在在的多態具體類,每個類都要有一個專門的虛函數表對應,這些可都要寫到二進制文件里面,代碼就是這樣膨脹起來的。總之,stl回避反射后,反射就以另一種形式回歸,好比virtual+template,好比%d、%s,好比locale的那些facet實現, 這些動態機制各自為政,各種混亂。還不如干脆就從源頭上系統化公理化地給予終極解決。
所以,總體上感受stl設計思路上存在的路線,就是太在意于c++語言本身上的特點,受語言自身的缺陷復雜影響太多,忽略了真正的需求,太多的臆造需求,強行讓需求來遷就語言,而不是讓語言來配合基礎庫的實際普遍需求,需求才是根本,為了可以最方便,最清晰,最性能的基礎庫,完全可以大規模地使用宏、挖掘語言里面最黑暗的邊角料,甚至為了庫的清晰性,可以拒絕那些用了復雜特性的數據結構,比如多繼承,虛繼承等無聊玩意。
概括起來,路線問題導致最終的正果,也即是stl的具體弱雞表現就是,最根本是二進制接口使用上的重重阻礙,誰敢在動態庫api使用stl的數據類型。其次是以下5小點:
1、內存分配器不應該是容器的模板參數,對allocator的處理太過草率,當初這里必須做深入的挖掘,c++完全可以實現一定程度上的垃圾回收功能,比如arean allocator,不必一一回收在arena allocator上分配的對象,只需一次性釋放arena allocator的內存,達到多次分配,一次釋放的高性能效果,還避免內存泄露,也不用直接面對循環引用的怪胎設計問題。現有的內存管理策略,把壓力都放在智能指針上;
2、提供的通用容器不夠完備;原本stl的數據結構就大可滿足所有正常和非正常的使用場合,比如滿足侵入式的鏈表需求,比如不管理元素生命周期的容器等;
3、過多的暴露迭代器,迭代器的應用范圍過廣,stl的算法函數用起來很不方便;
4、回避動態類型反射信息,對數據的輸入輸出支持非常單薄,包括字符串處理、文件讀寫、網絡數據收發等,標準庫上的現有那點小功能,僅僅是聊勝于無而已,難堪大任;
5、非容器系的實用類太少;
一句話,目前stl的使用,還是遠遠不夠爽。原本用上stl的代碼,應該可以更短、更快、更小。只可惜,stl在通過迭代器實現算法與容器的分離之后,就停步不前,其設計體系在別的地方,鮮有建樹創新。戰略高度過于局促,很多復雜難搞的問題,其實都蘊含著絕大的機遇,而stl都一一回避,真是回避得好!