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

一種向後兼容的C++結構體設計

內存拷貝 模板 發現 兼容 然而 eof 維護 類型 參數

問題產生的背景:
有時候,我們需要維護老舊代碼。這些代碼經常因為需求變更而變化。最常見的升級就是接口的升級,諸如增加新的函數接口、擴展函數的參數、擴展協議等等。在此我們討論一種較為少見的情形,即存儲於設備中的一段二進制結構的升級。這種情況類似於網絡通訊中的序列化,但又有所不同。關於如何設計序列化結構的文章有許多,我們在此不做討論。
設計目標:
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++結構體設計