现在来设计这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