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