现在来设计这3个函数的参数,我们要的是针对各种类型的库,所以很自然地,被编码和解码的数据类型应当为模板参数,这样才能适应各种数据类型的要求, 对应地,函数变成下面的模样:
Int GetSize ( T & data );
Void Encode ( T & data, char * & value );
Void Decode ( T & data, const char * & value );
Encode 和Decode 函数的第2个参数分别表示被编码成的内存指针和被解码的内存指针,返回的也是相应的内存指针。第1个参数就是模板参数了。我们先把这3个函数实现出来,从最简单的着手,我们假定T 为int, long之类的build-in类型,并且不为字符串。至于这3个函数是全局空间中的模板函数还是存在于一个模板类之中的模板函数,我们也先放一边。
Int GetSize ( T & data )
{
return sizeof(T);
}
Void Encode ( T & data, char * & value )
{
Memcpy (value, & data, sizeof(T) ); //对于如int之类的简单数据类型,直接内存拷贝
Value += sizeof(T); //移动内存指针
}
Void Decode ( T & data, const char * value )
{
Memcpy( & data, value, sizeof(T) );//对于如int之类的简单数据类型,直接内存拷贝
Value += sizeof(T); //移动内存指针
}
上面的实现是很简单的,在继续下去之前,有必要先交代一些我们的编码解码原则:
1. 所有的数据,都是以内存的方式进行编码,也就是说不会将数据先转化成字符串类型再编码;
2. 对于字符串类型,先编码它的长度,再编码字符串本身;
3. 对于容器,如 std::vector ,先编码它的元素个数,然后在逐个编码元素;
4. 数据不需要先编码元素个数,因为数组的元素个数是编译期可知的。
上面4点就是我们进行编码解码的原则,所以上面的3个实现函数对于int, long,unsigned int, unsigned long, char, unsigned char, short, unsigned short之类的数据类型全部都适用,但假如现在要编码的是一串字符,那么上面的实现就不能满足要求了。因为根据我们的编码原则的第2条,字符串要先编码长度,所以对于字符串,我们需要另外的实现。
进行到这里,我们需要回头来考虑一下这3个函数的存在方式了,是全局空间中的模板函数还是存在于某一个模板类中呢?
方法一:假如是存在于全局空间中,那么,对于字符串的编码解码来说,我们必须3个与上面名称不同的函数,或者是重载函数名称,提供3个针对字符串的非模板函数。
方法二:假如是存在于某一个模板类中,如EDSimple, 那么对于字符串来说,将EDSimple针对字符串进行全特化并实现这3个函数就可以了。
我们选择方法二,选择的理由将在后面来叙述。
依照方法二进行修改,就应该是下面的样子:
Template < class T>
Struct EDSimple
{
static int GetSize(T & data)
{
return sizeof(T);
}
static void Encode (T & data, char * & value)
{
memcpy(value,& data, sizeof(T));
value += sizeof(T);
}
static void Decode (T & data, const char * & value )
{
memcpy(&data, value, sizeof(T));
value += sizeof(T);
}
};
//下面是针对字符串进行全特化的实现
template<>
struct EDSimple<char *>
{
static int GetSize(char *& data)
{
return sizeof(int) + strlen(data);
}
static void Encode (char * & data, char * & value)
{
//先编码长度
int size = strlen(data);
memcpy(value, & size, sizeof(int));
value += sizeof(int);
if ( size > 0)
{
//再编码数据
memcpy(value, data, size);
value += size;
}
}
static void Decode (char * & data, const char * & value )
{
//先解码长度
int size = 0;
memcpy(&size, value, sizeof(int));
value += sizeof(int);
if ( size > 0 )
{
//再解码数据
data = new char[size+1];
memcpy(data,value,size);
data[size] = '/0';
value += size;
}
}
};
/
上面全特化只是针对char * , 对于const char *, 这要由自己来定了,这里不提供,因为const char * 在解码的时候是要强制转换的。考虑一下STL和MFC,我们还应该对CString和std::string做一下全特化处理,方式和上面一样,这里就不列了。
现在你有字符串需要编码的话,代码就要简单多了:
Std::string data (“this is a test string”);
//先获取长度,分配内存
int size = EDSimple<std::string>::GetSize ( data );
char * pData = new char[size];
char * p = pData;
//编码
EDSimple<std::string>::Encode ( data, pData );
//传输。。。
//接收到 pRecv
//解码:
std::string szRecv;
EDSimple<std::string>::Decode ( szRecv, pRecv );
容器:
到目前为止,我们的生活还是很幸福的,对于所有的build-in类型,CString和std::string,我们现在都有了方便的编码解码函数。考虑一下我们日常的使用,STL容器是相当频繁的。我们的库一定要支持容器。
STL容器分为关联式容器和序列式容器,列举一下:vector, list, map, set, stack, deque, queue, priority_queue. 另外有slist, mulset, mulmap之类的SGI提供的非标准C++要求的容器, 我们先不讨论,MFC提供的容器我们也不讨论。
对于容器的编码,根据我们的编码原则,先编码容器个数,再逐个编码元素内部的元素。元素个数都统一可以由size()函数获取到,容器内部的元素的统一获取方式就只有通过iterator了。因此,上面容器中的stack, queue, priority_queue这3个没有iterator的容器也不能获得支持。也就是说,对于容器,我们的目标就是vector, list, map, set, deque.
摩拳擦掌,很快地,我们的针对容器的编码类应该是这个模样:
Template<class T>
Struct EDContainer
{
static Int GetSize ( T & data );
static void Encode ( T & data, char * & value);
static void Decode ( T & data, const char * & value);
};
看起来也是很简单,先试着实现Encode 函数:
Static void Encode ( T & data, char * & value )
{
//容器个数
Int size = data.size (); // 问题:怎么知道这里的T一定是个STL容器???
}
出师不利,第一行代码就有一个棘手的问题,怎么知道模板参数一定就是一个STL的容器呢?并且还是我们支持的那几个STL容器,而不是stack, queue,priority_queue.
如果我们这么一个类多好:
Template<class T>
struct stl_container_traits
{
typedef true_type is_stl_container;
};
True_type是我们自己定义的:
Struct true_type {};
Struct false_type {};
这两个结构其实就是一个标记,像我们的true, false一样。True, false是运行期校验的,我们的true_type, false_type这两个结构我们可以根据它们的类型在编译期做检验。
有了stl_container_traits, 我们再来实现上面的Encode :
static void Encode ( T & data, char * & value )
{
Typedef stl_container_traits<T>::is_stl_container is_stl_container;
//问题:接下来怎么用is_stl_container来判断?
//容器个数
Int size = data.size (); // 问题:怎么知道这里的T一定是个STL容器???
}
呵呵,现在又多了一个新问题,不过不要怕,is_stl_container是一个类型,可能是true_type, 可能是false_type, 我们要对类型进行检验,这种编译期检验分流的办法通过函数的参数的办法:
static void Encode ( T & data, char * & value )
{
typedef stl_container_traits<T>::is_stl_container is_stl_container;
//问题:接下来怎么用is_stl_container来判断?
Encode ( data, is_stl_container() );
}
static void Encode ( T & data, char * & value, true_type )
{
//进入这个函数说明T一定是个STL容器
Int size = data.size (); // 没问题
}
static void Encode ( T & data, char * & value, false_type )
{
//进入这个函数说明T一定不是STL容器,或者不是我们能支持的STL容器
Int size = data.size (); 错误!!!
}
上面3个重载函数解决了我们的第一个问题,也就是怎么使用is_stl_container的问题。先别忙着高兴,我们的stl_container_traits 还是想象的,还没实现出来呢。在继续前进之前,先把这些拦路石搬开:
template<class T>
struct stl_container_traits
{
typedef true_type is_stl_container;
};
假如你阅读过SGI STL的源代码,或者是你阅读过候杰先生的《STL源码剖析》,你一定不会对traits陌生。我们现在只支持vector, list, map, set, deque, 所以对于这5种类型,我们希望 is_stl_container类型为true_type, 所以其它类型都为false_type. 这很容器,偏特化技巧可以帮我们实现:
template<class T>
struct stl_container_traits
{
typedef false_type is_stl_container;
};
//vector
template<class T>
struct stl_container_traits<std::vector<T> >
{
typedef os_true_type is_stl_container;
};
//map,list 类似
拦路石搬开了,继续前进:
static void Encode ( T & data, char * & value, true_type )
{
//进入这个函数说明T一定是个STL容器
Int size = data.size (); // 没问题
}
static void Encode ( T & data, char * & value, false_type )
{
//进入这个函数说明T一定不是STL容器,或者不是我们能支持的STL容器
Int size = data.size (); 错误!!!
}
对于第2个函数,由于T一定不是我们支持的容器,所以我们认为它是简单数据类型,或者为用户自定义类型,用户自定义类型又是一个大问题,不过先放一旁。简单数据类型的话,实现就很简单了:
static void Encode ( T & data, char * & value, false_type )
{
//T为简单数据类型,直接使用最开始的实现:
EDSimple<T>::Encode ( data, value ); //遗留问题:T 为用户自定义类型
}
重载函数Encode ( T & data, char * & value , true_type)的实现可就没有这么轻松了。
static void Encode ( T & data, char * & value, true_type )
{
//进入这个函数说明T一定是个STL容器
Int size = data.size (); // 没问题
//编码长度:
EDSimple<int>::Encode (size, value);
//对容器的每个元素进行编码
//取出T的迭代器类型
typedef T::iterator iterator;
//取出迭代器所指元素的类型
typedef std::iterator_traits<iterator>::value_type value_type;
//判断该迭代器所指向的元素是否也为STL容器
typedef stl_container_traits<value_type>::is_stl_container is_stl_container;
//对容器T的每一个元素循环进行编码
iterator end(data.end());
for (iterator iter = data.begin(); iter != end; ++ iter)
{
EDContainer<value_type>::Encode(*iter, value, is_stl_container());
}
}
上面的实现有两点至关重要:
1. 必须取出元素的类型并判断该元素是否也为容器,有可能有容器的嵌套,如:std::vector<std::vector<std::string> >
2. 必须递归调用EDContainer 自身,递归终结的地方就是当元素为非容器的时候;
Encode 函数到此实现完成。对应地,GetSize 和 Decode 函数也得提供两份,一份针对true_type, 一份针对false_type,这里就不列代码了。
三个小问题:
1. 编码和解码 std::pair 不支持。
解决这个问题不难,将EDSimple以std::pair全特化就可以。
2.对于我们不支持的容器,如果调用了我们的编码解码函数,我们需要一个编译期断言,这个简单,有很多,就不详述了。
2. 由于Decode是要还原的,所以涉及到将元素加入到容器中的操作,序列式容器如vector,list等是push_back操作,关联式容器是insert操作。这里进行区别的方法还是在stl_container_traits里面再安置一个类型 has_push_back, 偏特化一下处理一下,然后再提供两个辅助函数就可以了,这两个辅助函数就作为开胃小菜留给各位看官老大了。
包装:
到此,我们有了针对简单数据类型的EDSimple和针对容器的EDContainer,现在包装一下,提供一个上层统一接口:
template<class T>
struct EDdata
{
static int GetSize( T & data )
{
//先检查是否支持该类型
check_stl_container<T>();
typedef stl_container_traits<T>::is_stl_container is_stl_container;
return EDContainer<T>::GetSize(data, is_stl_container());
}
static void Encode(T & data, char * & value)
{
//先检查是否支持该类型
check_stl_container<T>();
typedef stl_container_traits<T>::is_stl_container is_stl_container;
EDContainer<T>::Encode (data, value, is_stl_container());
}
static void Decode(T & data, const char * & value)
{
//先检查是否支持该类型
check_stl_container<T>();
typedef stl_container_traits<T>::is_stl_container is_stl_container;
EDContainer<T>::Decode (data, value, is_stl_container());
}
};
为了使用上的方便(不需要每次都写模板参数,让编译器根据参数自己推导类型),再提供三个对应的辅助函数:
//Helper functions
template<class T>
int ED_GetSize(T & data )
{
return EDdata<T>::GetSize(data);
}
//Encode , Decode 类似
。。。。。。
检验一下成果,看看用得怎么样:
typedef std::vector<std::string> value_type;
value_type vec;
int size = ED_GetSize (vec);
char * pData = new char[size];
ED_Encode ( vec, pData);
。。。。。。
一切如预料中的一样!
用户自定义类型:
小王写了要把用户信息发送到服务器进行登陆,用户信息的结构如下:
Struct UserInfo
{
Int m_nId;
Std::string m_szName;
Std::string m_szPasswd;
};
小王要先把这个结构编码传输过去,服务器接收后解码并进行校验,我们看看小王编码的过程:
UserInfo info;
Int size = ED_GetSize (info.m_nId) + ED_GetSize(info.m_szName) + ED_GetSize(info.m_szPasswd);
Char * pData = new char[size];
ED_Encode (info.m_nId, pData);
ED_Encode (info.m_szName, pData);
ED_Encode (info.m_szPasswd, pData);
………………
还好这个结构不复杂,这点代码看着也能忍受。一旦这个结构变得复杂庞大,相应的编码代码就会立即铺满屏幕,这是不能忍受的,我们一开始就说过,我们要的就是方便,简单,一句话:要自动化。
再看上面的UserInfo, 里面有int, std::string.还可以是任何其他的类型。假如对UserInfo进行编码,我们可以将EDSimple全特化。
template<>
struct EDSimple<UserInfo>
{
Static void Encode ( UserInfo & data, char * & value )
{
ED_Encode ( data.m_nId, value);
ED_Encode ( data.m_szName, value);
ED_Encode ( data.m_szPasswd, value);
}
};
如果用户自己提供这么一个类当然是好,那一切运行都毫无问题。但我们的宗旨是不希望用户花时间在编写这个类上面,希望有一种好的机制能自动实现出这个类来,很显然,应当是宏了。
MFC的宏相当普遍,我们也来学一学它。
//这个宏是开始进行EDSimple的全特化定义
#define BEGIN_ED_USER_CLASS(class_name) /
template<> /
struct EDSimple<class_name> /
{
//这个宏是开始定义Encode函数
#define BEGIN_ENCODE_FUNCTION(class_name) /
static void Encode ( class_name & data, char * & p)/
{ /
//Encode函数的宏定义,只有1个成员的用户自定义类
#define ENCODE_USER_CLASS_MEMBER_1(class_name,N1) /
ED_Encode(data.##N1, p);
//Encode函数的宏定义,只有2个成员的用户自定义类
#define ENCODE_USER_CLASS_MEMBER_2(class_name,N1,N2) /
ENCODE_USER_CLASS_MEMBER_1(class_name, N1) /
ED_Encode(data.##N2, p);
//Encode函数的宏定义,只有3个成员的用户自定义类
#define ENCODE_USER_CLASS_MEMBER_3(class_name,N1,N2,N3) /
ENCODE_USER_CLASS_MEMBER_2(class_name, N1,N2)/
ED_Encode(data.##N3, p);
有了上面的宏,就简单了,再用一个宏把上面的几个宏组合一下:
#define ED_USER_CLASS_3(class_name, N1,N2,N3) /
BEGIN_ED_USER_CLASS(class_name) /
BEGIN_ENCODE_FUNCTION(class_name)/
ENCODE_USER_CLASS_MEMBER_3(class_name, N1,N2,N3) /
}/
};
现在,用户写完UserInfo后,想要得到自动编码解码支持,只需要定义一宏:
ED_USER_CLASS_3(UserInfo, m_nId, m_szName, m_szPasswd)
这就是我们的终极目标,HOHO ~~~~
现在我们要编码UserInfo,就可以像下面这样:
UserInfo info;
Int size = ED_GetSize(info);
Char * pData = new char[size];
ED_Encode (info, pData);
跟开始的方式一下,只少2行代码,但假如UserInfo类的成员变成10个,那就要8行代码了。现在不论成员变成多少个,编码就只需要这4行!
更进一步:
1. 数组,数组不是STL容器,但日常应用也很广泛,需要对它进行支持。这个通过重载就可以了;
2. std::bitset, 这个如果应用中需要的话,也可以加入到EDSimple中去;
3. 加入MFC的各种容器;
4. 编码方式,我们的编码是根据前面列的几条编码原则进行的。假如编码原则要改变,比如我们想要编码成XML文档,只需要改变相应的EDSimple实现就可以,上层代码不需要改动。可以参考Policy的设计方式,提供几种EDSimple的实现,根据需要自行选择:
template< template<class T> class EDPolicy >
2006-12-31 北京