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

Материал из Весельчак У
Перейти к: навигация, поиск

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

Говоря о сериализации, я подразумиваю механизмы перевода некоторого класса, структуры или набора переменных в определённый формат (бинарный, текстовый, 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.

Большого смысла приводить примеры и описывать все возможности, всё очень хорошо описано в документации.

За универсальность 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);
	}
};

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