前幾天回爺爺家重裝系統(tǒng),在整理舊硬盤的時候找到了我早年寫的一些破爛代碼。查看了下時間,這些代碼大多書寫于2012-2013年間,那段時間我是個什么樣的狀態(tài)呢?使勁回憶了一下,那段時間我是一個小透明(當(dāng)然我現(xiàn)在仍然是一個小透明)。當(dāng)時我大專畢業(yè)一年,書寫的代碼都很簡陋幼稚,經(jīng)不起推敲。那段時期,從好友同事WK處學(xué)習(xí)了與跨平臺相關(guān)的一個C++的語言技巧,這思路影響了我很長一段時間代碼書寫的方式。
是什么樣的跨平臺語言技巧?
我們都知道 DirectX 這套API只能在Windows上使用。想要在 Linux / MacOS 相關(guān)系統(tǒng)上進(jìn)行渲染,我們需要使用不同的API比如:OpenGL / Metal。但是我仍然希望我的程序,如果運(yùn)行在windows平臺上的時候,使用 Direct3D進(jìn)行渲染,怎么辦呢?
代碼寫兩遍!這當(dāng)然沒有什么魔法可言,但是寫兩遍代碼的方法卻有許多種。2012年的我,一直都只知道把所有的API直接寫在程序里。這意味著假如我需要換個API的話,我得把程序里使用API的每一處地方都找到,然后再全都改掉,有時候甚至需要對結(jié)構(gòu)做出一些修正。這是多么可怕的一件事情!當(dāng)時WK告訴我,許多時候我們并非直接使用API,而是通過C++的純虛函數(shù)來包裝一層,解決這個問題。
怎么通過純虛函數(shù)來包裝API ?
最一開始,我們需要定義一個BaseClass,去描述我們需要使用的功能,一般我把他叫做一個“接口”,這就包含了1)一組純虛函數(shù),作為接口函數(shù)使用;為了讓析構(gòu)正確,我們需要顯式地定義一個2)虛析構(gòu)函數(shù)和一個3)Release接口。
class Interface
{
public:
inline ~virtual Interface() { }
virtual void Release() = 0;
virtual const char* GetName() = 0;
};
接下來,我們在不同的文件中去實現(xiàn)這個接口:
// in InterfaceDX.cpp
class InterfaceDX : public Interface
{
public:
virtual const char* GetName() override { return "direct3d";}
virtual void Release() override { delete this; }
};
// in InterfaceGL.cpp
class InterfaceGL : public Interface
{
public:
virtual const char* GetName() override { return "opengl";}
virtual void Release() override { this->~InterfaceGL(); free(this); } //see below
};
從代碼可知,這兩個不同的文件會分別使用不同的(Direct3D / OpenGL)API,會引入不同的頭文件。而對于用戶,他只需要關(guān)心class Interface這個接口定義,并不需要關(guān)心這個接口是通過哪個API實現(xiàn)的。當(dāng)然最終我們不會把這兩個CPP放在一起編譯,我們?nèi)匀恍枰屵@些不同的實現(xiàn)提供同一個 Factory 接口的實現(xiàn):
class Interface
{
public:
static Interface* Create();
public:
inline ~virtual Interface() { }
virtual void Release() = 0;
virtual const char* GetName() = 0;
};
// in InterfaceGL.cpp
Interface* Interface::Create() { return new InterfaceDX(); }
// in InterfaceGL.cpp
Interface* Interface::Create() {
auto p = (InterfaceGL*) malloc( sizeof(InterfaceGL) );
p->InterfaceGL();
return p;
}
我們把這兩個CPP放在不同的項目里,分別成不同的鏈接庫(Whatever,隨你喜歡) ?,F(xiàn)在我們就獲得了兩個不同的DLL/SO,其中一個是使用Direct3D實現(xiàn)的DLL,而另一個,則是使用OpenGL實現(xiàn)的SO。在宿主程序里,我們根據(jù)系統(tǒng)判斷的結(jié)果,加載對應(yīng)版本的模塊;然后通過查詢Interface::Create接口,構(gòu)造出處Interface對象,宿主程序的用戶客戶只能獲取到一個Interface的指針,亦只需要關(guān)心Interface的接口。
為何提供虛析構(gòu)和額外的Release接口?
有時候我們需要通過 BaseClass 的指針去引用一個 DerivedClass 的對象,我們在通過 BaseClassPtr 去釋放 DerivedClassObject 的時候,需要正確遞歸調(diào)用析構(gòu)函數(shù)。正如以上情況,提供虛析構(gòu)函數(shù)是必要的??紤]如下代碼:
Interface* pObject = new InterfaceDX();
// ... omitted ...
delete pObject();
析構(gòu)函數(shù)如果不是虛函數(shù),則系統(tǒng)會調(diào)用 BaseClass 的默認(rèn)析構(gòu),不會產(chǎn)生遞歸效果,導(dǎo)致了 DerivedClassObject(或者繼承自Derived的子類對象)的析構(gòu)函數(shù)沒有被調(diào)用而產(chǎn)生潛在的內(nèi)存泄露。
而涉及CRT的問題,有可能DLL和宿主程序使用不相同的CRT,這意味著DLL和宿主程序之間內(nèi)存地址是分離的,此時指針是夸CRT使用的。遵循一個簡單的原則:誰申請誰釋放。我們提供一個顯式的內(nèi)存釋放的Release接口,讓申請內(nèi)存的對象去釋放內(nèi)存,以保證釋放地址的正確性。
基本上,接口的構(gòu)造告一段落。本質(zhì)上這么做是把實現(xiàn)細(xì)節(jié)隱藏起來了,因為大部分平臺相關(guān)的頭文件,都被囊括在實現(xiàn)文件里,而對于接口頭文件而言,是很干凈的。不僅能夠快速的替換不同的實現(xiàn)以達(dá)到跨平臺的目的;同時獲得了隱藏細(xì)節(jié)的好處,不對外暴露很多實現(xiàn)用的結(jié)構(gòu)體、和內(nèi)部對象。另外需要注意的是,接口頭文件中盡量使用原生類型,而不要使用STL或者別的復(fù)雜類型,以避免DLL與宿主程序之間出現(xiàn)STL版本不相符合導(dǎo)致錯誤的問題。
無地自容
盡管現(xiàn)在看起來這是一個常識,大部分的庫都會用這樣或者那樣的姿勢包裝。對于2012年的我來說,無異于打開了一扇奇妙的大門。我開始嘗試許多有意思實踐,比如用C/C++/NDK去實現(xiàn)一個FileSystem;用DX/GL/GLES實現(xiàn)了一個 GraphicDeviceInterface(難道這就是GDI的全拼嗎);還有Socket、thread、system相關(guān)的亂七八糟的小玩具。不僅如此,我還在同年開通了GITHUB賬號。盡管現(xiàn)在看起來這些破爛一無是處,只是推著我不斷熟悉代碼的味道。
時光荏苒,我也稍微了解一些不同的OO名詞,什么duck type,closure云云的拼寫方式,讓我對面向?qū)ο缶幊逃钟辛烁畹恼J(rèn)識。在踽踽獨(dú)行的路上,也遇到了突破天際的大神,讓我長跪不起。然而這些都不妨礙我現(xiàn)在查閱起以前代碼的時候,會臉紅害臊。
其中印象深刻的是一個智商148的大神,他告訴我要多看世界上厲害的人寫代碼,這樣才能夠取長補(bǔ)短。我一直不以為然,直到最近,我又遇到一個95后的大神,給我推開了模板元編程這個閃光的大門。我終于認(rèn)識到,假如當(dāng)年WK沒有告訴純虛函數(shù)這個書寫方式,我現(xiàn)在可能還是一只小透明(當(dāng)然現(xiàn)在確實仍然是小透明)。智商148說的是正確的,我最近又在瘋狂的書寫著各種讓人看不懂的模板。
然而我在推演未來一定會對現(xiàn)在寫的代碼感到害臊到無地自容的情況時,我突然想起了我最開始的上司跟我說過的這么一句話:寫代碼就和說話一樣,說清楚就行了。不要用那些文縐縐的字眼人家可能根本聽不懂。我一直把這個理論當(dāng)做真理,停留在舒適區(qū)里。但或許我這位上司也一定經(jīng)歷過了很多次我這樣的瘋狂練習(xí),才說出這樣的話吧!那他眼里的代碼又是什么樣子的呢?
僅此紀(jì)念我瞎折騰的2012。(和潛在可能的瞎折騰的2016?)
ps. duck type
我是從 typescript中才了解到 duck type 這個概念,并沒有深究。也就只能隨便說說。說到這個duck type,很是有趣:一般的我們看到一只鴨子,都長著羽毛、腳上有蹼、喙又長又硬、能在水里游,還能聽到嘎嘎叫。那么我們把任何觀察到的物體,只要符合以上這些特征,都認(rèn)為是一只鴨子(或許它有可能是一直鵝、或者鴛鴦。)或許其實我們應(yīng)該用水禽type更加合適一些……
這意味著,我們需要定義這樣一個接口:
interface object2d
{
float area;
vector2 origin;
};
這意味著,只要我們的對象中有 area / origin這兩個屬性,我就就能把他認(rèn)為是一個 object2d。比如:
class circle
{
float area() { return r*r*π; }
vector2 origin;
};
class rect
{
float left, top, width,height;
float area() { return width*height; }
vector2 origin() { return (left + 0.5 * width, top + 0.5 * height); }
};
這意味著我在某個函數(shù)需要一個 object2d 的時候,我可以直接把 circle 或者 rect 當(dāng)做參數(shù)傳入函數(shù)內(nèi)。當(dāng)然,這個語法C++還不支持。但是這給我們提供了一個視野,就像薛定諤的貓一樣,你怎么觀測circle/rect,他就會根據(jù)你的觀察、表現(xiàn)出特定樣子。