祖?zhèn)髌拼a

前幾天回爺爺家重裝系統(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)出特定樣子。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 一、溫故而知新 1. 內(nèi)存不夠怎么辦 內(nèi)存簡單分配策略的問題地址空間不隔離內(nèi)存使用效率低程序運(yùn)行的地址不確定 關(guān)于...
    SeanCST閱讀 8,117評論 0 27
  • 動態(tài)調(diào)用動態(tài)庫方法c/c++linuxwindows 關(guān)于動態(tài)調(diào)用動態(tài)庫方法說明 一、 動態(tài)庫概述 1、 動態(tài)庫的...
    KINGZ1993閱讀 14,182評論 0 10
  • 1. 讓自己習(xí)慣C++ 條款01:視C++為一個語言聯(lián)邦 為了更好的理解C++,我們將C++分解為四個主要次語言:...
    Mr希靈閱讀 2,993評論 0 13
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,868評論 25 709
  • 今天工作特別煩忙有點(diǎn)累回到家看兒子在玩,我問他你的課文背過了嗎?兒子說沒背過我就沖孩子發(fā)火了訓(xùn)了他一頓沒背過還不敢...
    放飛心情_a7c7閱讀 88評論 0 0

友情鏈接更多精彩內(nèi)容