序列化与反序列化在编程中是最常见不过了,项目中原来写的序列化与反序列化是使用的普通类成员函数来写的,每一个数据类型都写了一个operator<<以及operator>>函数,整个代码相当长,而且冗余度高。
假如我们一个Stream类如下:
class Stream
{
public:
// 序列化
virtual void Serialize(void* data, size_t size)
{
// 实现略
}
// 反序列化
virtual void Deserialize(void* data, size_t size)
{
// 实现略
}
};
如果我们重载<<符号来进行序列化,重载>>符号来进行反序列化,使用普通类成员函数的写法,以int类型为例就是:
class Stream
{
public:
// 序列化
virtual void Serialize(void* data, size_t size)
{
// 实现略
}
// 反序列化
virtual void Deserialize(void* data, size_t size)
{
// 实现略
}
Stream& operator<<(const int v)
{
Serialize((void*)&v, sizeof(v));
return *this;
}
Stream& operator>>(int& v)
{
Deserialize(&v, sizeof(v));
return *this;
}
};
C++中有bool,char, unsigned char,short, unsigned short,int,unsigned int,long long,unsigned long long等等许多标准类型,还有枚举类型以及自定义类型。如果每一个类型都写一个函数去重载,代码量还是非常大的,而且函数除了参数类型不一样外,其它都一样,相当冗余。这种情况非常适合使用C++的模板,特别是C++11之后,可以直接使用标准库的模板函数来进行类型匹配。
在C++11之前,可能需要自己来写一系列的模板判断哪些类型是标准类型,根据是否是标准类型让编译器来判断匹配哪个函数;C++11就可以直接使用std::is_XXX系列函数来进行判断了。
在写出实现前,我们需要了解一下什么是SFINAE:
SFINAE是Substitution Failure Is Not An Error的缩写,直译为中文是:替换失败不是错误,cppreference中的定义为:
在函数模板的重载决议中:为模板形参替换推导类型失败时,从重载集抛弃特化,而非导致编译错误。
我的理解就是编译器在匹配重载函数时,通过类型的推导可能匹配到类型不一致的重载函数而导致匹配失败,但是只要最后能正确匹配到一个重载函数,编译器就不会报错。使用过Erlang语言的读者会对函数匹配有比较深刻的理解。
这里有一个非常关键的模板函数std::enable_if,它会让编译器根据条件来判断该重载函数是否是所需要的函数。std::enable_if 可以使用在以下三个方面:
作为函数参数:
template<typename T>
struct Check1
{
template<typename U = T>
U read(typename std::enable_if<
std::is_same<U, int>::value >::type* = 0) {
return 42; }
template<typename U = T>
U read(typename std::enable_if<
std::is_same<U, double>::value >::type* = 0) {
return 3.14; }
};
作为模板参数:
template<typename T>
struct Check2
{
template<typename U = T, typename std::enable_if<
std::is_same<U, int>::value, int>::type = 0>
U read() {
return 42; }
template<typename U = T, typename std::enable_if<
std::is_same<U, double>::value, int>::type = 0>
U read() {
return 3.14; }
};
作为返回类型:
template<typename T>
struct Check3
{
template<typename U = T>
typename std::enable_if<std::is_same<U, int>::value, U>::type read() {
return 42;
}
template<typename U = T>
typename std::enable_if<std::is_same<U, double>::value, U>::type read() {
return 3.14;
}
};
了解了SFINAE(替换失败不是错误)就可以使用C++11来重写前面的冗余代码了。
在C++11中对枚举进行了扩展,以前的枚举定义是:
enum XXX
{
AA,
BB
};
现在添加了enum class XXX,
enum class XXX
{
AA,
BB
};
两者的区别就是前者可以直接在代码中引用AA,BB,编译器把它们当作整数来对待;后者不能直接在代码中引用AA,BB,要引用必须添加XXX::限定符,即XXX::AA,XXX::BB,编译器不再把它们当作整数来对待,所以不能与整数进行比较,要与整数比较必须进行强转。
所以如果要序列化enum class枚举,编译器不再匹配为整数,而是需要单独处理。所以如果不想在使用的时候不断写强转代码,普通类成员函数是无法满足各种枚举类型的序列化与反序列化的,只能使用模板函数来处理:
template<typename T>
Stream& operator<<(typename std::enable_if<std::is_enum<T>::value, Stream>::type &stream, const T& v)
{
return stream << static_cast<typename std::underlying_type<T>::type>(v);
}
这个模板使用std::is_enum模板函数来检测模板参数T是否是枚举类型,配合std::enable_if模板函数让编译器匹配枚举类型。实现代码中再使用std::underlying_type模板函数将T转换成标准的类型,默认是int类型,如果枚举从其它标准类型派生则为基类类型,比如:
enum class XXX : char
就是char类型。
处理了枚举类型,就需要处理前面提到的标准类型,C++11提供了std::is_integral模板函数来判断是否是除浮点类型外的基本类型,而浮点类型使用std::is_floating_point模板函数来判断浮点类型,同时提供了std::is_arithmetic模板函数来判断前面两个函数包含的所有类型。所以我们使用下面的模板函数来处理这些类型:
template<typename T>
Stream& operator<<(typename std::enable_if<std::is_arithmetic<T>::value, Stream>::type &stream, const T& v)
{
stream.Serialize((void*)&v, sizeof(T));
return stream;
}
template<typename T>
Stream& operator>>(typename std::enable_if<std::is_arithmetic<T>::value, Stream>::type &stream, T& v)
{
stream.Deserialize((void*)&v, sizeof(T));
return stream;
}
这里序列化有一点问题就是如果有强转,如果模板匹配没写对,可能会导致编译通不过,比如
uint8 n;
stream << (uint16)n;
这是因为在强制转换时从左值变为右值了,所以需要一个右值引用的函数:
template<typename T>
Stream& operator<<(typename std::enable_if<std::is_arithmetic<T>::value, Stream>::type &stream, const T&& v)
{
stream.Serialize((void*)&v, sizeof(T));
return stream;
}
编译器在正常情况下会自动进行转换,比如自动添加const进行匹配,右值可以匹配到左值引用。后面的代码清单就是利用了编译器的这些自动转换。
这样几个函数就把使用普通成员类函数所能表达的所有标准类型以及枚举类型的情况处理完了,是不是非常简洁?
然后就是自己定义的类型的处理了,这里需要作一个规定,就是自己定义的类型如果要序列化与反序列化,必须要有:
// 序列化
void Serialize(Stream &stream) const
{
// 实现略
}
// 反序列化
void Deserialize(Stream &stream) const
{
// 实现略
}
这两个类成员函数的实现,这样就可以使用下面的模板函数来处理自定义类了:
template<typename T>
Stream& operator<<(typename std::enable_if<std::is_member_function_pointer<decltype(&T::Serialize)>::value, Stream>::type &stream, const T& v)
{
v.Serialize(stream);
return stream;
}
template<typename T>
Stream& operator>>(typename std::enable_if<std::is_member_function_pointer<decltype(&T::Deserialize)>::value, Stream>::type &stream, T& v)
{
v.Deserialize(stream);
return stream;
}
这里使用了std::is_member_function_pointer模板函数来判断类型T是否有Serialize函数以及Deserialize函数,如果有则匹配,没有则不匹配。
如果自定义类型没有Serialize与Deserialize的函数实现,比如STL的容器类型,可以继续使用普通的类成员函数,也可以提出来,这些不再使用模板函数来处理,而是需要单独实现重载。
下面列出经过优化后的代码清单:
#include <iostream>
typedef unsigned int uint;
class Stream
{
public:
// 序列化
virtual void Serialize(void* data, size_t size)
{
// 实现略
}
// 反序列化
virtual void Deserialize(void* data, size_t size)
{
// 实现略
}
};
// 序列化
// 该函数可以匹配char*, const char*, std::string以及const std::string
static Stream& operator<<(Stream &stream, const std::string& v)
{
uint len = (uint)v.length();
stream.Serialize(&len, sizeof(uint));
stream.Serialize((void*)v.c_str(), len);
return stream;
}
// 该函数可以匹配算术类型以及枚举类型
template<typename T>
Stream& operator<<(typename std::enable_if<std::is_arithmetic<T>::value || std::is_enum<T>::value, Stream>::type &stream, const T& v)
{
stream.Serialize((void*)&v, sizeof(T));
return stream;
}
// 该函数匹配所有自定义类中有void Serialize(Stream &stream) const函数实现的类型
template<typename T>
Stream& operator<<(typename std::enable_if<std::is_member_function_pointer<decltype(&T::Serialize)>::value, Stream>::type &stream, const T& v)
{
v.Serialize(stream);
return stream;
}
// 反序列化
// 该函数可以匹配算术类型以及枚举类型
template<typename T>
Stream& operator>>(typename std::enable_if<std::is_arithmetic<T>::value || std::is_enum<T>::value, Stream>::type &stream, T& v)
{
stream.Deserialize(&v, sizeof(T));
return stream;
}
// 该函数匹配所有自定义类中有void Deserialize(Stream &stream) const函数实现的类型
template<typename T>
Stream& operator>>(typename std::enable_if<std::is_member_function_pointer<decltype(&T::Deserialize)>::value, Stream>::type &stream, T& v)
{
v.Deserialize(stream);
return stream;
}
// 该函数只能匹配std::string
static Stream& operator>>(Stream &stream, std::string& v)
{
uint len = 0;
stream.Deserialize(&len, sizeof(uint));
v.resize(len);
stream.Deserialize(&v[0], len);
return stream;
}
以上是使用的类外重载操作符来实现的,如果是使用类的成员函数应该怎么写呢?下面列出代码清单:
#include <iostream>
typedef unsigned int uint;
class Stream
{
public:
// 序列化
virtual void Serialize(void* data, size_t size)
{
// 实现略
}
// 反序列化
virtual void Deserialize(void* data, size_t size)
{
// 实现略
}
// 序列化
Stream& operator<<(const std::string& v)
{
uint len = (uint)v.length();
Serialize(&len, sizeof(uint));
Serialize((void*)v.c_str(), len);
return *this;
}
template<typename T, std::enable_if_t<std::is_arithmetic<T>::value || std::is_enum<T>::value> * = nullptr>
Stream& operator<<(const T& v)
{
Serialize((void*)&v, sizeof(T));
return *this;
}
template<typename T, std::enable_if_t<std::is_member_function_pointer<decltype(&T::Serialize)>::value> * = nullptr>
Stream& operator<<(const T& v)
{
v.Serialize(*this);
return *this;
}
// 反序列化
Stream& operator>>(std::string& v)
{
uint len = 0;
Deserialize(&len, sizeof(uint));
v.resize(len);
Deserialize(&v[0], len);
return *this;
}
template<typename T, std::enable_if_t<std::is_arithmetic<T>::value || std::is_enum<T>::value> * = nullptr>
Stream& operator>>(T& v)
{
Deserialize(&v, sizeof(T));
return *this;
}
template<typename T, std::enable_if_t<std::is_member_function_pointer<decltype(&T::Deserialize)>::value> * = nullptr>
Stream& operator>>(T& v)
{
v.Deserialize(*this);
return *this;
}
};
是不是感觉特别简洁,代码量非常小。这里的类成员函数是使用函数签名的方式来实现的,模板参数中enable_if_t成立则等效于:
template <typename T, void * = nullptr>
在书写时需要注意的是*与=之间必须有一个空格,不然会出现编译错误。
以上代码在VS2015、GCC 4.9.3、Clang 9.0编译器编译通过。
参考:
https://zh.cppreference.com/w/cpp/language/sfinae
https://stackoverflow.com/questions/14600201/why-should-i-avoid-stdenable-if-in-function-signatures