一种向后兼容的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++的角度来看,上述思路还有许多改进的地方,在此仅做抛砖引玉,欢迎各位的讨论