一種向後兼容的C++結構體設計
問題產生的背景:
有時候,我們需要維護老舊代碼。這些代碼經常因為需求變更而變化。最常見的升級就是接口的升級,諸如增加新的函數接口、擴展函數的參數、擴展協議等等。在此我們討論一種較為少見的情形,即存儲於設備中的一段二進制結構的升級。這種情況類似於網絡通訊中的序列化,但又有所不同。關於如何設計序列化結構的文章有許多,我們在此不做討論。
設計目標:
1. 為了兼容老版本的結構體
2. 為了支持內存拷貝初始化
3. 版本號的支持
4. 盡量少的代碼修改
假設我們第一次(舊)的數據結構如下:
struct Old{ int i; };
首先,我們期望能對後續升級的結構體帶有版本號。最簡單的想法是在結構體中添加一個int類型的版本信息。但是,當我們深入考慮時,首先想到的一個問題就是,我們該如何從一段內存區中得到這個版本信息。如果我們添加了版本字段,那麽我們首先需要找到這個字段,得到其版本號,然後再把這個緩沖區的數據轉換成對應版本的數據結構。顯然,我們是知道這個字段所在的內存偏移量的。於是我們的實現代碼大概如下:
struct V1{ int i; int version; //version==1 }; struct V2{ int i; int version; //version==2 int j; }; // unsigned char* buff=new unsigned char[100]; int len = 0; getStruct(buff,&len); int* pVersion = &(buff[4]);
於是我們拿到了結構體的版本號,可以根據版本號得到具體的數據類型了。然而仔細考察一下可以發現,實際上我們並不需要這個版本號,因為每一次升級,數據結構都是在原有的基礎上添加的,因此這個結構體的長度會隨著版本號的增加而增加,所以我們可以利用這個結構體的長度(註意對其可能導致長度相同的問題),來作為區分版本的關鍵。於是,我們省去了一個int的長度。
為了能夠區分版本,我們在上面的結構體名字當中使用了諸如1、2之類的標誌。實際上,我們可以利用C++語法的模板來代替這些常量,以確保代碼的易讀性。於是結構體的定義更改為:
template<int VERSION> struct V{}; template<> struct V<0>{ int i; } template<> struct V<1>{ int i; int j; };
為了保證能夠與C的結構體兼容,我們還需要保證我們的結構體是POD類型。因此我們不能在結構體中定義任何初始化函數,也不能使用繼承。為了保證這一規範,我們采用靜態斷言,提前為未來的升級做約束:
static_assert(std::is_pod<V<0> >::value==true,"V<0> is not a POD type"); static_assert(std::is_pod<V<1> >::value==true,"V<1> is not a POD type"); static_assert(std::is_pod<V<2> >::value==true,"V<2> is not a POD type"); static_assert(std::is_pod<V<3> >::value==true,"V<3> is not a POD type");
於是我們可以這樣去使用這個結構體:
getStruct(buff,&len); switch(len){ case sizeof(V<0>): { V<0>* pV=(V<0>*)buff; } break; case sizeof(V<1>): { V<1>* pV=(V<1>*)buff; } break;
}
從C++的角度來看,上述思路還有許多改進的地方,在此僅做拋磚引玉,歡迎各位的討論
一種向後兼容的C++結構體設計