【C++】Boost.Serialization疑难解答:缓冲、动态数组等

工作中可能面临如下需求:

  1. 将结构化数据,比如类,通过网络传输到其他地方。
  2. 高成本计算得到的数据,要临时保存到磁盘中,希望下一次读取进来直接就是格式化好的数据;而不再需要一个个地读取进来,在内存中重建构建数据结构。

我们可以用序列化/反序列化来完成上述任务。序列化是指,将用复杂数据结构构建的数据,变为字符串或者二进制流;反序列化则是相反过程,直接从字符串或者二进制流中完全恢复内存现场。
目前常用的C++序列化库有Google Protobuf,Boost.Serialization,Thrift。根据反映的情况,效率上Protobuf最强,Thrift围绕序列化的功能最丰富,Boost.Serialization则直接支持STL容器的序列化。编码上讲,protobuf较难,Boost.Serialization最简单。还有资料说,Protobuf环境构建最简单,Boost最难。不过Boost的prebuilt binary package在sourceforge上随时可以下载到,解压无需编译就可用全部功能。
Boost.Serialization的教程很多,不过大部分是官网教程的中文翻译。实际上官网教程比较初级,实用到业务上还不够。这里解答几个业务上可能遇到的疑问。

如何序列化动态数组

根据资料,Boost.Serialization不能直接序列化数组,必须将数组包括到一个类中。教程中,Boost.Serialization可直接序列化类中的固定数组:

class bus_route
{
    friend class boost::serialization::access;
    bus_stop * stops[10];
    template<class Archive>
    void serialize(Archive & ar, const unsigned int version)
    {
        ar & stops;
    }
public:
    bus_route(){}
};

显然,相当多情况下,我们会new一个动态数组来使用,上述教程就没明说怎么做。为了序列化动态数组,我们必须先定义数组长度,然后用for循环进行序列化:

template<typename T>
struct Array {
	Array() { arr_ = 0; len_ = 0; }
	~Array() { if (arr_) delete[] arr_; arr_ = 0; len_ = 0; }
	T* arr_;
	size_t len_;
};

使用非侵入式方法来写:

namespace boost {
	namespace serialization {

		template<typename Archive, typename T>
		void serialize(Archive & ar, Array<T>& g, const unsigned int version)
		{
			ar & g.len_;
			if (g.arr_ == 0) {
				g.arr_ = new T[g.len_];
			}
			for (size_t i = 0; i < g.len_; ++i) {
				ar & g.arr_[i];
			}
		}
	}
}

先要序列化和反序列化动态数组的长度信息,然后恢复数组数据。serialize函数既用于序列化也用于反序列化。

进行多次序列化/反序列化

内存序列化的典型代码如下:

std::stringstream ss;、
boost::archive::text_oarchive oa(ss);
oa << serializa_object1;//序列化
boost::archive::text_iarchive ia(ss);
ia >> deserializa_object1;

这里用std::stringstream ss作为序列化辅助工具,在调试模式下我们可以监视ss的内容,看到object被序列化后的字符串内容。接下来,我们再次进行序列化操作,然后会报错:

std::stringstream ss;
boost::archive::text_oarchive oa(ss);
oa << serializa_object1;//序列化
boost::archive::text_iarchive ia(ss);
ia >> deserializa_object1;
oa << serializa_object2;//序列化第二次
ia >> deserializa_object2;//报错

在调试模式下可以看到,ss的内容没变,程序报错。因此我们要重初始化std::stringstream ss才行:

std::stringstream ss;
boost::archive::text_oarchive oa(ss);
oa << serializa_object1;//序列化
boost::archive::text_iarchive ia(ss);
ia >> deserializa_object1;
ss.clear();
oa << serializa_object2;//序列化第二次
ia >> deserializa_object2;//正确
序列化到指定缓冲区

在网络传输业务中,我们需要把序列化的内容放到char*缓冲区中,让asio传输数据。如果使用std::stringstream ss作为序列化辅助工具,那么数据内容需要从ss中提取:

std::stringstream ss;
boost::archive::text_oarchive oa(ss);
oa << serializa_object1;//序列化
std::string content = ss.str();//提取内容

然后std::string content就可以进行后续处理,比如直接用content.c_str()当作asio缓冲区,或者讲数据复制到char *buffer。
问题来了,序列化要转录一遍,效率未免有点低?如果直接让Boost.Serialization把序列化数据存放到指定的char *buffer,从指定的char *buffer反序列化数据,效率就高了。一个可用代码如下:

char i_buffer[4096];
char o_buffer[4096];//缓冲区

boost::iostreams::basic_array_sink<char> sr(o_buffer, 4096);
boost::iostreams::stream< boost::iostreams::basic_array_sink<char> > os(sr);//序列化用

boost::iostreams::basic_array_source<char> device(i_buffer, 4096);
boost::iostreams::stream<boost::iostreams::basic_array_source<char> > is(device);//反序列化用

Array<int> arr;
arr.len_ = 10;
arr.arr_ = new int[arr.len_];
int id = 0;
std::for_each(arr.arr_, arr.arr_ + 10, [&](int &arg) {arg = ++id; });//填充数据

std::for_each(arr.arr_, arr.arr_ + 10, [](int &arg) {std::cout << arg << " "; });
std::cout << std::endl;//打印

boost::archive::binary_oarchive oa(os);
oa << arr;

memcpy(i_buffer, o_buffer, 4096);//模拟网络传输

Array<int> darr;
boost::archive::binary_iarchive ia(is);
ia >> darr;//反序列化
std::for_each(darr.arr_, darr.arr_ + 10, [&](int &arg) {std::cout << arg << " "; });
std::cout << std::endl;
//重初始化,以重复利用
memset(i_buffer, 0, 4096);
memset(o_buffer, 0, 4096);
os.close();
os.open(sr);
is.close();
is.open(device);

std::for_each(arr.arr_, arr.arr_ + 10, [&](int &arg) {arg = ++id; });
oa << arr;

memcpy(i_buffer, o_buffer, 4096);

ia >> darr;
std::for_each(darr.arr_, darr.arr_ + 10, [&](int &arg) {std::cout << arg << " "; });
std::cout << std::endl;

这样我们序列化的数据都固定存放在char* buffer上,不再需要从std::string转一道。

模板库序列化

Eigen是一个基于模板技术的数学库。因为模板技术,它无需编译即可运行,但是编译期报错就头疼,此外对于序列化也造成一定困扰。比如可能理所当然的写出下面的错误代码:

template<typename Archive, typename T>
void serialize(Archive &ar, Eigen::Matrix3d &mat, const unsigned int version) {//报错!!
	for(int i = 0; i < 3; ++i){
		for(int j = 0; j < 3; ++j){
			ar & mat(i,j);
		}
	}
}

原因是,Eigen::Matrix3d不是真实类名,这是一个带有很长模板参数的类。那么正确代码应该是:

template<   class Archive,
	class S,
	int Rows_,
	int Cols_,
	int Ops_,
	int MaxRows_,
	int MaxCols_>
	inline void save(
		Archive & ar,
		const Eigen::Matrix<S, Rows_, Cols_, Ops_, MaxRows_, MaxCols_> & g,
		const unsigned int version)
{
	int rows = g.rows();
	int cols = g.cols();

	ar & rows;
	ar & cols;
	ar & boost::serialization::make_array(g.data(), rows * cols);
}

template<   class Archive,
	class S,
	int Rows_,
	int Cols_,
	int Ops_,
	int MaxRows_,
	int MaxCols_>
	inline void load(
		Archive & ar,
		Eigen::Matrix<S, Rows_, Cols_, Ops_, MaxRows_, MaxCols_> & g,
		const unsigned int version)
{
	int rows, cols;
	ar & rows;
	ar & cols;
	g.resize(rows, cols);
	ar & boost::serialization::make_array(g.data(), rows * cols);
}

template<   class Archive,
	class S,
	int Rows_,
	int Cols_,
	int Ops_,
	int MaxRows_,
	int MaxCols_>
	inline void serialize(
		Archive & ar,
		Eigen::Matrix<S, Rows_, Cols_, Ops_, MaxRows_, MaxCols_> & g,
		const unsigned int version)
{
	split_free(ar, g, version);//让读取和加载分为两个函数
}

然后我们就可以直接序列化Eigen库了。
然而后面还有一个问题,如果把Eigen对象作为类成员,那么还有问题在:

struct Data {
	double val_;
	Eigen::Vector3d vec_;
};

template<typename Archive>
void serialize(Archive &ar, Data &res, const unsigned int version) {
	ar & res.val;
	ar & res.vec_;//报错!!!
}

这是因为隶属于Data类的serialize函数,带有复杂的模板参数。遗憾的是,我还没找到解法,目前可行的策略是用侵入式写法:

struct Data {
	double val_;
	Eigen::Vector3d vec_;
	friend class boost::serialization::access;
	template<class Archive>
	void serialize(Archive & ar, const unsigned int version)
	{
		ar & val_;
		ar & vec_;//正确
	}
};
常见报错
  1. 报错“invalid signature”,是serialize函数出了问题,比如没有编写serialize函数,或者找不到可以重载serialize函数,比如上面对Eigen矩阵的序列化,要填入长长的模板参数才是正确的serialize函数。
  2. 报错“length_error”,尤其是boost::archive::binary_iarchive ia对象初始化的时候报错。这是因为ia在初始化的时候,就已经读取了缓冲区数据并进行解析,缓冲区的第一个数据指代了需要读取缓冲区的长度,如果缓冲区没有数据,那么就会报错。因此要在数据传入缓冲区后,才能构建一个ia对象。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章