考不上三本也能懂系列——實現C++型別系統
你現在所閱讀的並不是第一篇文章,你可能想看目錄和前言。
這個標題有點大。當然我根本就沒打算要跟你們介紹C++的型別系統是怎麼一回事,我只想簡單介紹一下如何來用資料結構表達C++的型別系統,讓計算進行地更高效。不過在這裡先說一點題外話。我們都知道,int[1][2][3]是一個數組。C#的陣列是沒有尺寸的,所以它只能表達成int[][][]或者int[,,]。那麼如果我們給C#的陣列加上尺寸,那C++的int[1][2][3]在C#裡面應該怎麼寫呢?
答案很簡單:要麼寫成int[1,2,3],要麼寫成int[3][2][1]。至於為什麼,你們可以在拉尿的時候花10秒鐘思考一下,沒想出來的都不及格,趕緊改行(逃
本來我想寫《 考不上三本也能懂系列——處理宣告(二) 》的,但是我發現在模板還沒做出來之前,剩下的內容太少了,寫不成一篇文章,所以以後再寫。
今天要講的內容其實都在 ofollow,noindex">這裡 。C++的程式從頭跑到尾需要識別很多型別,我們需要用資料結構來表達他們。顯而易見地,型別是一棵樹。為了讓編譯的時間縮短,我們在建立、銷燬、比較型別的時候,都要儘可能快。於是今天我介紹一種簡單的做法。
在這裡假設我們只有int、指標和陣列三種類型。我的編譯器會忽略陣列的尺寸,因為為了code index搞constexpr的展開實在是划不來,乾脆不做了,int[1]和int[2]我都看成是同一個型別。
為了讓程式設計更愉快,我們定製介面的時候要遵循一種實用的態度,也就是這個介面使用起來必須容易,而且我們還不能為了容易使用而扭曲設計。因此最後我選擇了下面的一種做法:
enum class TypeEnum { Int, Pointer, Array, }; struct IType { virtual TypeEnum GetType() = 0; virtual IType* GetElement() = 0; // Pointer和Array有用,Int呼叫它直接崩潰 virtual IType* PointerOf() = 0; // 建立這個型別的指標型別,下同 virtual IType* ArrayOf() = 0; }; struct ITypeSystem { static shared_ptr<ITypeSystem> Create(); virtual GetInt() = 0; };
使用的時候很簡單。加入我們需要表達int*(*)[],這是一個int的指標的陣列的指標,我們只需要這麼做:
IType* GetComplexType(shared_ptr<ITypeSystem> tsys) { return tsys->GetInt()->PointerOf()->ArrayOf()->PointerOf(); } // Assert(GetComplexType(fuck) == GetComplexType(fuck)); // Assert(GetComplexType(fuck)->GetElement()->GetElement() == fuck->GetInt()->PointerOf());
在這裡shared_ptr<ITypeSystem>是一個共享的物件,呼叫這個函式就可以得到int*(*)[]的資料結構的表達。為了滿足上面所說的要求,呼叫兩次GetComplexType必須返回的是同一個指標,因此比較兩個型別是否相等就超級容易了,直接比較指標就好了,快的一筆。而且這還特別節省記憶體。那到底要怎麼實現呢?其實非常簡單,我們在每個型別裡面都快取下來它的指標和陣列的型別就好了:
struct Base : IType { IType* pointerOf = nullptr; IType* arrayOf = nullptr; IType* PointerOf() override { if (!pointerOf) { pointerOf = MakeType<Pointer>(this); } return pointerOf; } // ArrayOf 同上 }; struct Int : Base { TypeEnum GetType()override { return TypeEnum::Int; } IType* GetElement()override { throw "Fuck!"; } }; struct Pointer : Base { TypeEnum GetType()override { return TypeEnum::Pointer; } IType* GetElement()override { return element; } // MakeType<Pointer>(傳進來的) }; struct Array : Base { TypeEnum GetType()override { return TypeEnum::Array ; } IType* GetElement()override { return element; } };
MakeType函式,就是跟ITypeSystem的實現要求從記憶體池裡面構造一個那樣的物件出來。剩下來的事情就很簡單了。型別建立之後是不用釋放的,編譯器跑完了之後只要把ITypeSystem一刪,全部東西都灰飛煙滅,記憶體絕對不可能洩露。這個事情做起來就很方便了。我們有Int、Pointer和Array三個型別,分別製作三個連結串列,每個連結串列有譬如說1024個節點。一開始只是一段記憶體,然後你就可以在需要的時候placement new一個出來,然後順手counter++。如果滿了就弄下一個節點接著來。釋放的時候很容易,每個節點呼叫夠placement delete之後直接幹掉。這也是STL實現vector等容器的原理,是每一個C++程式設計師都應該熟練掌握的技巧。這部分的程式碼在 這裡 。
今天的內容比較簡單。接下來還有一些有趣的內容,譬如說如何確保我們的編譯器在做一些型別運算的結果(譬如說a+b)的時候,跟VC++保持完全一致,並且test case寫出來還要嚴格遵守Don't Repeat Yourself的原則。還有譬如說,定義二元運算表示式的語法都是左遞迴的,我們要怎樣才能把這麼多的左遞迴全部做在一個迴圈裡。還有很多其他的細節都是很有意思的,接下來會給大家一一介紹。