【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對象。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章