FAQ:STL:C++ сериализация данных

Материал из Весельчак У
Версия от 01:41, 4 августа 2015; RXL (обсуждение | вклад)

(разн.) ← Предыдущая | Текущая версия (разн.) | Следующая → (разн.)
Перейти к: навигация, поиск

Иногда нужно сохранить состояние класса в файл, передать состояние класса по сети. Подобные задачи обычно решает сериализация.

Говоря о сериализации, я подразумеваю механизмы перевода некоторого класса, структуры или набора переменных в определённый формат (бинарный, текстовый, XML, HTML и т.д.), а также сам процесс перевода. Десериализация - процессы и механизмы, обратные сериализации (восстановление состояния из внешнего источника).

Самый простой способ, к которому чаще всего прибегают молодые программисты, - это простое копирование памяти в файл или еще куда-то. Т.е. берём указатель на класс/структуру/переменную и копируем N байт в файл. Пример:
.......
MyClass m;
..............
std::ofstream f("dump.bin", std::binary);
f.write(&m, sizeof(m));
f.close();
........................

Но этот метод сериализации имеет ряд ограничений и недостатков:

  • Допустимо использовать только для POD-структур (POD - Plain Old Data) и встроенных типов. Почему, будет понятно из следующих пунктов.
  • Если программистом описан конструктор, то компилятор вправе в класс добавить какие-то свои вспомогательные переменные, что превращает класс в не POD-структуру, на самом деле это не так страшно, но формально это так.
  • При сохранении указателей членов класса будут скопированы только адреса, хранимые указателями и, естественно, класс с указателями это не POD-тип
  • Если в классе объявленные виртуальные функции (или он унаследован от класса содержащего виртуальные функции), это приводит к тому, что класс будет дополнен указателем на таблицу виртуальных функций, и с этим указателем та же проблема, что и со всеми другими. Опять же не POD-тип.
  • Если ваш класс содержит внутри себя не POD типы или унаследован от не POD-типа, то ваш класс тоже не под тип, т.е. нет никакой гарантии, что копирование куска памяти позволит постановить состояние класса.
  • Различное выравнивание данных внутри класса может сделать невозможным перенос сохранённого класса на другую платформу или даже в программу, скомпилированную с другими параметрами компиляции.
  • Различный порядок байт не позволит переносить данные между такими платформами, как: x86 и PowerPC
  • И такая сериализация не даёт сохранить в удобочитаемой форме XML, текст или CSV

Есть много ситуаций, когда создание дампа памяти - вполне допустимый метод сериализации, но также есть другое множество ситуаций, когда такой подход недопустим.

Как только программист задумывается о сериализации данных, ему сразу же хочется выполнять сериализацию всего парой строк кода: легко и изящно, например, так:
// вывести состояние класса и всех его членов.
std::cout << myClass;
 
// загрузить состояние класса из XML
myXML >> myClass;

И, естественно, самый простой способ быстро добиться результата - это использовать "велосипед", написанный другими. "Велосипед" возьмём хороший, многофункциональный. Он умеет выполнять сериализацию и десериализацию стандартных контейнеров, классов, указателей, ссылок и еще чего-то. Также он умеет сохранять, работать с различными форматами выходных данных: бинарный, текст, XML. Если очень хочется, то он может сохранить не только в поток, но и куда угодно, например, в вектор или в сокет или выкинуть в пропасть.

Полное описание "велосипеда": http://www.boost.org/doc/libs/1_36_0/libs/serialization/doc/index.html

Вот пример использования (взято из описания):

/////////////////////////////////////////////////////////////
// gps координаты
//
// illustrates serialization for a simple type
//
class gps_position
{
private:
    friend class boost::serialization::access;
    // When the class Archive corresponds to an output archive, the
    // & operator is defined similar to <<.  Likewise, when the class Archive
    // is a type of input archive the & operator is defined similar to >>.
    template<class Archive>
    void serialize(Archive & ar, const unsigned int version)
    {
        ar & degrees;
        ar & minutes;
        ar & seconds;
    }
    int degrees;
    int minutes;
    float seconds;
public:
    gps_position(){};
    gps_position(int d, int m, float s) :
        degrees(d), minutes(m), seconds(s)
    {}
};
 
int main() {
    // create and open a character archive for output
    std::ofstream ofs("filename");
 
    // create class instance
    const gps_position g(35, 59, 24.567f);
 
    // save data to archive
    {
        boost::archive::text_oarchive oa(ofs);
        // write class instance to archive
        oa << g;
    	// archive and stream closed when destructors are called
    }
 
    // ... some time later restore the class instance to its orginal state
    gps_position newg;
    {
        // create and open an archive for input
        std::ifstream ifs("filename");
        boost::archive::text_iarchive ia(ifs);
        // read class state from archive
        ia >> newg;
        // archive and stream closed when destructors are called
    }
    return 0;
}

Теперь по пунктам, как это работает:

  1. Создаём внутри нашего класса метод serialize, получаем ссылку на архив и номер версии(можно по-разному сериализовать в зависимости от версии), если метод приватный, то добавляем в друзья class boost::serialization::access. Метод serialize будет вызываться при сериализации и десериализации.
  2. Открываем файл и создаём архив (text_oarchive текстовый выходной архив), в нашем случае текстовый, архив - это тот самый класс, который выполняет основную работу.
  3. Вызываем всеми любимый оператор << - этот оператор вызывает метод serialize для классов или же внешние функции(они идут в комплекте) для встроенных типов и стандартных контейнеров.
  4. text_oarchive::operator<< вызвал наш метод serialize и передал вовнутрь себя, тут возникает вопрос: почему внутри serialize используется оператор &, а не <<? Ответ: потому что у выходного архива операторы & и << по сути это одно и тоже, у входного операторы & и >> - одно и тоже. Т.е. ничто не мешает в коде поменять "ia >> newg;" на "ia & newg;".
  5. Если нужно изменить метод сериализации, достаточно поменять тип архива (для XML архива придётся сделать еще некоторою работу в методе serialize).

На этом, в общем-то, работа по поддержке сериализации закончена.

При желании, можно разделить методы для сериализации и десериализации. Кстати, boost::seralization гарантирует, что порядок байт при сериализации будет изменён, если потребуется, так что можно спокойно передать long с x86 на PowerPC.

Немного поправленный пример использования и результаты работы:

#include "stdafx.h"
#include <iostream>
#include <fstream>
#include <string>
#include <fstream>
#include <vector>
 
// include headers that implement a archive in simple text format
#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
#include <boost/archive/binary_iarchive.hpp>
#include <boost/archive/binary_oarchive.hpp>
#include <boost/archive/xml_iarchive.hpp>
#include <boost/archive/xml_oarchive.hpp>
// включаем, чтобы сериализация работала с векторами
#include <boost/serialization/vector.hpp>
// включаем, чтобы нормально проходила сериализация XML
#include <boost/serialization/nvp.hpp>
 
class gps_position
{
private:
	friend class boost::serialization::access;
 
	template<class Archive>
	void serialize(Archive & ar, const unsigned int version)
	{
		// то же, что и make_nvp, только имя параметра выводится в макросе
		ar & BOOST_SERIALIZATION_NVP(degrees);
		ar & BOOST_SERIALIZATION_NVP(minutes);
		ar & BOOST_SERIALIZATION_NVP(seconds);
	}
	int degrees;
	int minutes;
	float seconds;
public:
	gps_position(){};
	gps_position(int d, int m, float s) :
	degrees(d), minutes(m), seconds(s)
	{}
};
 
template<typename TIArch, typename TOArch, typename TClass>
void TestArch(const std::string & file, int flags, const TClass & cont)
{
 
	{ // Сериализуем
		std::ofstream ofs(file.c_str(), std::ios::out|flags);
		TOArch oa(ofs);
		// make_nvp создаёт пару имя-значение, которая отразится в XML
		// если не используем XML архив, то можно пару не создавать
		oa << boost::serialization::make_nvp("Test_Object", cont); 
	}
 
	TClass newg;
	{ // Десериализуем
		std::ifstream ifs(file.c_str(), std::ios::in|flags);
		TIArch ia(ifs);
		ia >> boost::serialization::make_nvp("Test_Object",newg);
	}
 
	{ // Еще раз сериализуем, чтобы потом сравнить результаты двух сериализаций
		// и убедиться, что десериализациия прошла корректно
		std::ofstream ofs((file+".tmp").c_str(), std::ios::out|flags);
		TOArch oa(ofs);
		oa << boost::serialization::make_nvp("Test_Object", cont);
	}
}
 
int _tmain(int argc, _TCHAR* argv[])
{
	std::ofstream ofs("filename");
 
	std::vector<gps_position> v;
	v.push_back(gps_position(35, 59, 24.567f));
	v.push_back(gps_position(36, 60, 25.567f));
	v.push_back(gps_position(37, 61, 26.567f));
 
	using namespace boost::archive;
	TestArch<text_iarchive, text_oarchive>("text_arch.dump", 0, v);
	TestArch<binary_iarchive, binary_oarchive>("binary_arch.dump", std::ios::binary, v);
	TestArch<xml_iarchive, xml_oarchive>("xml_arch.dump", 0, v);
 
	return 0;
}
Файл text_arch.dump:
22 serialization::archive 3 0 0 3 0 0 35 59 24.566999 36 60 25.566999 37 61 26.566999

Как видите, использование NVP никак не отразилось на внешнем виде архива.

Файл xml_arch.dump:
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<!DOCTYPE boost_serialization>
<boost_serialization signature="serialization::archive" version="3">
<Test_Object class_id="0" tracking_level="0" version="0">
	<count>3</count>
	<item class_id="1" tracking_level="0" version="0">
		<degrees>35</degrees>
		<minutes>59</minutes>
		<seconds>24.566999</seconds>
	</item>
	<item>
		<degrees>36</degrees>
		<minutes>60</minutes>
		<seconds>25.566999</seconds>
	</item>
	<item>
		<degrees>37</degrees>
		<minutes>61</minutes>
		<seconds>26.566999</seconds>
	</item>
</Test_Object>
</boost_serialization>

Бинарный архив приводить не стану :) не очень красочно, но занимает он 79 байт, из которых 39 - заголовок и 40 - полезная информация.


За универсальность boost::serialization придётся заплатить:

  • Во время компиляции шаблоны могут разворачиваться довольно долго.
  • Скорость: стек вызовов для сериализации какой-нибудь не слишком больной структурки, может быть просто ужасающим - вызовов 20-30.

Но если вы не пишете систему массового обслуживания, то это то, что вам нужно, с помощью этой библиотеки можно даже реализовать маршалинг или RPC.


Надеюсь, в общих чертах понятно, как примерно работает сериализация и десериализация, и если понадобится, можно реализовать свою сериализацию.


Вот пример своей реализации архива, который я использую вместо boost:binary_iarchive (была ОЧЕНЬ важна скорость, а универсальность не очень, но для маршалинга хватает), делал так, чтобы можно было использовать один вместо другого без переделки кода:

class binary_iarchive
{
public:
	typedef serialization::container container;
	typedef container::iterator iterator;
 
	container &cont_;
	size_t currentPos_;
	typedef boost::mpl::bool_<false> is_saving;
	binary_iarchive(container & cont, long = 0)
			: cont_(cont)
			, currentPos_(0)
	{
	}
 
	template<typename T>
	binary_iarchive & operator>>(T & val)
	{
		deserialize_impl(val);
		return *this;
	}
 
	void reset()
	{
		resetPos();
		cont_.clear();
	}
 
	template<typename T>
	inline void raw_read(T beginPos, size_t len)
	{
		if (static_cast<size_t>(cont_.size() - currentPos_) < len)
			throw std::runtime_error("No more data");
 
		iterator pos = cont_.begin() + currentPos_;
		iterator endPos = pos + len;
		std::copy(pos, endPos, beginPos);
		currentPos_ = currentPos_ + len;
	}
private:
	// Fundamental
	template<typename T>
	inline void deserialize_impl(T & val, typename boost::enable_if<boost::is_fundamental<T> >::type* dummy = 0)
	{
		raw_read(reinterpret_cast<char*>(&val), sizeof(T));
	}
 
	//Classes
	template<typename T>
	inline void deserialize_impl(T & val, typename boost::enable_if<boost::is_class<T> >::type* dummy = 0)
	{
		deserialize_class(*this, val);
	}
};

Кое-что порезал, чтобы не расслаблялись :)