FAQ:STL:C++ сериализация данных — различия между версиями

Материал из Весельчак У
Перейти к: навигация, поиск
Строка 1: Строка 1:
Иногда нужно сохранить состояние класса в файл, передать состояние класса по сети. Подобные задачи обычно решает сериализация.
+
Иногда нужно сохранить состояние класса в файл, передать состояние класса по сети. Подобные задачи обычно решает сериализация.  
  
Говоря о сериализации, я подразумиваю механизмы перевода некоторого класса, структуры или набора переменных в определённый формат (бинарный, текстовый, XML, HTML и т.д.), а так же сам процесс перевода. Десериализация - процессы и механизмы обратные сериализации (восстановление состояния из внешнего источника).
+
Говоря о сериализации, я подразумиваю механизмы перевода некоторого класса, структуры или набора переменных в определённый формат (бинарный, текстовый, XML, HTML и т.д.), а так же сам процесс перевода. Десериализация - процессы и механизмы обратные сериализации (восстановление состояния из внешнего источника).  
  
Самый простой способ, к которому чаще всего прибегают молодые программисты - это простое копирование памяти в файл или еще куда-то.
+
Самый простой способ, к которому чаще всего прибегают молодые программисты - это простое копирование памяти в файл или еще куда-то. Т.е. берём указатель на класс/структуру/переменную и копируем N байт в файл. Пример: <syntaxhighlight lang="cpp">
Т.е. берём указатель на класс/структуру/переменную и копируем N байт в файл. Пример:
+
<syntaxhighlight lang="cpp">
+
 
.......
 
.......
 
MyClass m;
 
MyClass m;
Строка 15: Строка 13:
 
</syntaxhighlight>  
 
</syntaxhighlight>  
  
Но этот метод сериализации имеет ряд ограничений и недостатков:
+
Но этот метод сериализации имеет ряд ограничений и недостатков:  
*Допустимо использовать только для POD-структур (POD - Plain Old Data) и встроенных типов. Почему будет понятно из следующих пунктов.
+
 
*Если, программистом описан конструктор, то компилятор в праве в класс добавить какие-то свои вспомогательные переменные, что превращает класс в не POD-структуру, на самом деле это не так страшно, но формально это так.
+
*Допустимо использовать только для POD-структур (POD - Plain Old Data) и встроенных типов. Почему будет понятно из следующих пунктов.  
*При сохранении указателей членов класса, будут скопированы, только адреса хранимые указателями и естественно класс с указателями это не POD-тип
+
*Если, программистом описан конструктор, то компилятор в праве в класс добавить какие-то свои вспомогательные переменные, что превращает класс в не POD-структуру, на самом деле это не так страшно, но формально это так.  
*Если в классе объявленные виртуальные функции (или он унаследован от класса содержащего виртуальные функции), приводит к тому, что класс будет дополнен указателем на таблицу виртуальных функций, и с этим указателем, таже проблема, что и со всеми другими. Опять же не POD-тип.
+
*При сохранении указателей членов класса, будут скопированы, только адреса хранимые указателями и естественно класс с указателями это не POD-тип  
*Если ваш класс содержит внутри себя не POD типы или унаследован от не POD-типа, то ваш класс тоже не под тип, т.е. нет никакой гарантии, что копирование куска памяти позволит постановить состояние класса.
+
*Если в классе объявленные виртуальные функции (или он унаследован от класса содержащего виртуальные функции), приводит к тому, что класс будет дополнен указателем на таблицу виртуальных функций, и с этим указателем, таже проблема, что и со всеми другими. Опять же не POD-тип.  
*Различное выравнивание данных внутри класса может сделать не возможным перенос сохранённого класса на другую платформу или даже в программу скомпилированную с другими параметрами компиляции.
+
*Если ваш класс содержит внутри себя не POD типы или унаследован от не POD-типа, то ваш класс тоже не под тип, т.е. нет никакой гарантии, что копирование куска памяти позволит постановить состояние класса.  
 +
*Различное выравнивание данных внутри класса может сделать не возможным перенос сохранённого класса на другую платформу или даже в программу скомпилированную с другими параметрами компиляции.  
 
*Различный порядок байт не позволит переносить данные между такими платформами, как: x86 и PowerPC  
 
*Различный порядок байт не позволит переносить данные между такими платформами, как: x86 и PowerPC  
 
*И такая сериализация не даёт сохранить в удобочитаемой форме: XML, текст или CSV
 
*И такая сериализация не даёт сохранить в удобочитаемой форме: XML, текст или CSV
  
Есть куча ситуация, когда создание дампа памяти вполне допустимый метод сериализации, но так же, есть другая куча ситуаций когда такой подход не допустим.
+
Есть куча ситуация, когда создание дампа памяти вполне допустимый метод сериализации, но так же, есть другая куча ситуаций когда такой подход не допустим.  
  
Как только программист задумывается о сериализации данных, ему сразу же хочется выполнять сериализацию всего парой строк кода: легко и изящно, например, так:
+
Как только программист задумывается о сериализации данных, ему сразу же хочется выполнять сериализацию всего парой строк кода: легко и изящно, например, так: <syntaxhighlight lang="cpp">
<syntaxhighlight lang="cpp">
+
 
// вывести состояние классы и всех его членов.
 
// вывести состояние классы и всех его членов.
 
std::cout << myClass;
 
std::cout << myClass;
Строка 36: Строка 34:
 
</syntaxhighlight>  
 
</syntaxhighlight>  
  
И естественно самый простой способ быстро добиться результата это использовать велосипед написанный другими. Велосипед возьмём хороший много функциональный. Он умеет выполнять сериализацию и десериализацию: стандартных контейнеров, классов, указателей, ссылок и еще чего-то. Также он умеет сохранять работать с различными форматами выходных данных: бинарный, текст, XML. Если очень хочется, то он сможет может сохранить не только в поток, но и куда угодно, например, в вектор или в сокет или выкинуть в пропасть.
+
И естественно самый простой способ быстро добиться результата это использовать велосипед написанный другими. Велосипед возьмём хороший много функциональный. Он умеет выполнять сериализацию и десериализацию: стандартных контейнеров, классов, указателей, ссылок и еще чего-то. Также он умеет сохранять работать с различными форматами выходных данных: бинарный, текст, XML. Если очень хочется, то он сможет может сохранить не только в поток, но и куда угодно, например, в вектор или в сокет или выкинуть в пропасть.  
  
Полное описание велосипеда: http://www.boost.org/doc/libs/1_36_0/libs/serialization/doc/index.html
+
Полное описание велосипеда: http://www.boost.org/doc/libs/1_36_0/libs/serialization/doc/index.html  
  
 
Вот пример использования (взято из описания):  
 
Вот пример использования (взято из описания):  
Строка 99: Строка 97:
 
     return 0;
 
     return 0;
 
}
 
}
</syntaxhighlight>
+
</syntaxhighlight>  
  
Для поддержки в наших классах мы должны в них реализовать метод serialize. Что он делает он в сериализатор(в терминах boost это archive) передаёт все внутренние данные, в свою очередь архив для простых типов делает копирование в памяти (кстати следит при этом за порядком байт, так что можно спокойно переносить на файл на другую машину с другой архитектурой), для сложных зовёт метод serialize, для стандартных контейнеров тоже, есть методы сериализации для контейнеров простых типов происходит копирование элементов, для контейнеров сложных типов происходит вызов serialize для каждого элемента. Для сериализации контейнеров придётся подключить дополнительные заголовки.
+
Теперь по пунктам как это работает:
  
Можно разделить методы для сериализации и десериализации, есть поддержка версий архивов, есть макросы для подписи элементов в текстовом файле и XML есть примитивы для создания своих форматов (или я повторяюсь) и еще много чего, классная библиотека.  
+
#создаём внутри нашего класса метод serialize получает ссылку на архив и номер версии(можно по разному сериализовать в зависимости от версии), если метод приватный, то добавляем в друзья class boost::serialization::access. Метод serialize будет вызываться при сериализации и десериализации
 +
#открываем файл и создаём архив (text_oarchive текстовый выходной архив), в нашем случае текстовый - архив это тот самый класс который выполняет основную работу
 +
#вызываем всеми любимый оператор << - этот оператор вызывает метод serialize для классов или же внешние функции(они идут в комплекте) для встроенных типов и стандартных контейнеров
 +
#text_oarchive::operator<< вызвал наш метод serialize и передал во внутрь себя, тут возникает вопрос: Почему внутри serialize используется оператор &, а не <<? Ответ: потому, что у выходного архива операторы & и << по сути это одно и тоже, у входного операторы & и >> одно и тоже. Т.е ничто не мешает в коде поменять "ia >> newg;" на "ia & newg;"
 +
#если нужно изменить метод сериализации достаточно поменять тип архива (для XML архива придётся сделать еще некоторою работу в методе serialize)
  
Но за универсальность boost::serialization придётся заплатить:  
+
на это в общем, то работа по поддержке сериализации закончена.
 +
 
 +
При желании, можно разделить методы для сериализации и десериализации. Кстати boost::seralization гарантирует, что порядок байт при сериализации будет изменён, если потребуется, так что можно спокойно передать long с x86 на PowerPC.
 +
 
 +
Большого смысла приводить примеры и описывать все возможности, всё очень хорошо описано в документации.
 +
 
 +
За универсальность boost::serialization придётся заплатить:  
  
 
*Время компиляции шаблоны могут разворачиваться довольно долго  
 
*Время компиляции шаблоны могут разворачиваться довольно долго  
 
*Скорость: стек вызовов для сериализации какой-нибуть не слишком больной структурки может быть просто ужасающим вызовов 20-30.
 
*Скорость: стек вызовов для сериализации какой-нибуть не слишком больной структурки может быть просто ужасающим вызовов 20-30.
  
Но, если вы не пишете систему массового обслуживания, то это то, что вам нужно с помощью этой библиотеки можно даже реализовать маршалинг или RPC, как больше нравится, т.е. я в своей программе создаю класс, но вместо собственно класса получаю gate к этому классу созданному на другой машине в момент когда я его создал на своей и я могу работать с созданным на другой машине классом, как с созданным на своей, при этом спокойно пользуюсь вызовами с передачей параметров по ссылке или указателю и знаю, что всё будет сериализовано - передано - обработано - передано обратно - десериализовано, так, что я не замечу подмены.
+
Но, если вы не пишете систему массового обслуживания, то это то, что вам нужно с помощью этой библиотеки можно даже реализовать маршалинг или RPC.  
 
+
<br> Понятно, как примерно работает сериализация и десериализация, и если понадобится можно реализовать свою сериализацию.  
+
  
<br> Можно, например, реализовать у объектов метод ToString который будет звать такой же метод у всех своих потомком, что бы сериализовать объект в строку.  
+
<br> Надеюсь в общих чертах, понятно, как примерно работает сериализация и десериализация, и если понадобится можно реализовать свою сериализацию.  
  
<br> Вот пример своей реализации архива, который я использую вместо бустового (ибо скорость ОЧЕНЬ важна, а универсальность не очень, но для маршалинга хватает), но делал так, что бы можно было использовать один вместо другого без переделки кода:  
+
<br> Вот пример своей реализации архива, который я использую вместо boost:binary_iarchive (была нужна скорость ОЧЕНЬ важна, а универсальность не очень, но для маршалинга хватает), но делал так, что бы можно было использовать один вместо другого без переделки кода:  
  
 
<syntaxhighlight lang="cpp">
 
<syntaxhighlight lang="cpp">

Версия 09:02, 29 сентября 2008

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

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

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