1. 程式人生 > >一種向後相容的C++結構體設計

一種向後相容的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++的角度來看,上述思路還有許多改進的地方,在此僅做拋磚引玉,歡迎各位的討論